5 1* {3 Järjestelmäohjelmoinnin alkeiskurssi - Osa 2 {3 -------------------------------------------- {3 System Executive - ohjelmointi {3 ------------------------------ Sami Klemola Kurssin ensimmäisessä osassa tutustuttiin tietokoneen toimintaan yleisellä ta- solla sekä alustavasti Execin tehtäviin. Tässä ainakin toistaiseksi viimeisessä osassa selviää, miten ohjelmia tehdään Amiga-ympäristöön. Artikkelissa sivutaan myös konekieliohjelmointia, mutta pääpaino on C-kielessä. Tässä osassa käydään myös läpi Execin osat yksi kerrallaan, ja jokaisesta tulee esimerkkikoodia. En- siksi kuitenkin katsotaan ohjelmoinnin vaiheita. {3Ohjelmoinnin työkalut ja kääntäminen {3------------------------------------ Tarvitset kääntäjän ja linkkerin. Jos ohjelmoit konekielellä, tarvitset makroas- semblerin. Jos kirjoitat C-koodia, tarvitset lisäksi C-kääntäjän, mutta myös edelleen makroassemblerin, koska C-kääntäjä ei tuota valmista koodia, vaan AS- CII-muotoista assemblyä eli konekielen lähdekoodia. C-kääntäjän mukana voi tulla erityisesti sen tuottamaa koodia kääntämään tarkoitettu makroassembleri. Sitten käännetään vielä makroassemblerilla eli konekielikääntäjällä, aivan kuin C- kääntäjän tuottama tiedosto olisi itse kirjoittamaasi konekielilähdekoodia. Mak- roassembleri ei sekään tuota vielä ajettavaa ohjelmaa, vaan objektikooditiedos- ton. Tämän jälkeen tulee vielä yksi vaihe, linkkaus. Linkkeri tulee usein mak- roassemblerin mukana, mutta niitä on saatavilla erillisinäkin. Linkkeri yhdistää makroassemblerin tuottaman objektikoodin sekä tarvittaessa linkkerikirjastosta mukaan rutiineja, joita se kutsuu. Linkkeri hakee myös kir- jastokutsujen osoituksien arvot koodiin ja C-koodin tapauksessa liittää mukaan tarvittavan startup-koodin, joka mm. tulkitsee komentorivin valmiiksi ja alustaa I/O:n. Kaikki nämä ajetaan usein yhdellä komennolla, joka voi ajaa skriptin tai frontend-ohjelman. Esimerkki tällaisesta tulee myöhemmin. Ohjelmaan voi myös kuulua useita lähdekoodeja, joista jokainen käännetään omaksi objektikooditie- dostokseen. Linkkeri yhdistää silloin nämäkin tiedostot ja asettaa niistä toi- siinsa tehdyt osoitukset eli cross referencet, ristiviittaukset. Muuta tarpeellista Includet ovat olennainen osa ohjelmointia. Ne ovat tiedostoja, jotka määritte- levät käyttöjärjestelmän sekä muiden ohjelmien käyttämiä datarakenteita ym. Au- todocit ovat kuvauksia esimerkiksi käyttöjärjestelmän kirjastofunktioista. Niistä käy ilmi kullekin funktiolle annettavat parametrit, mitä funktio tekee ja mitä se palauttaa. Myös rekisterit, joihin parametrit sijoitetaan, näkyvät auto- doceissa. Tieto siitä hyödyttää erityisesti konekieliohjelmoijaa. Palautusarvo tulee aina D0:ssa. Jotkin funktiot voivat palauttaa kaksikin arvoa, jolloin toi- nen palautetaan yleensä D1:ssä, ja sen saa myös DOS:n kautta kyselemällä jälkikäteen. Lisäksi tarvitaan amiga.lib, joka on nk. linker library. Tähän asti on puhuttu vain ajonaikaisista kirjastoista, jotka ohjelma avaa kutsuakseen niiden funk- tioita. Ajonaikaiset kirjastot sijaitsevat LIBS:-hakemistossa. Linker library on aivan erilainen kirjasto. Ohjelman käyttämät rutiinit, jotka sijaitsevat kirjas- tossa, linkataan ohjelmatiedostoon mukaan, joten kirjastoa ei tarvita ohjelmaa ajettaessa. Tällaisten funktioiden käyttäminen tietysti pidentää ohjelmaa huo- mattavasti. Siksi onkin aina syytä käyttää vastaavaa ajonaikaisen kirjaston ru- tiinia, jos sellainen vain on olemassa. Lisäksi amiga.lib sisältää ajonaikaisten kirjastojen offset-arvot. Näitä arvoja tarvitaan kutsuttaessa kirjaston funktioita. Offset lisätään kirjaston kantao- soitteeseen, jolloin saadaan hyppyosoite funktioon. Osoite on tosin vain hyppy- taulukkoon, jossa on yleensä suoraan hyppy varsinaiseen koodiin. Offset-arvot ovat negatiivisia, koska hyppytaulukko sijaitsee ja kasvaa kirjaston kantaosoit- teesta alaspäin. Kirjastofunktioiden käyttämisestä tulee esimerkkejä ja lisää tietoa myöhemmin. Ohjelman kääntäminen Kuten on jo mainittu, ohjelma käännetään yleensä frontendilla. Itselläni on oma- tekoinen cc-ohjelma, joka ajaa edelleen dcc:n, joka on varsinainen DICE:n fron- tend. Käytän cc:tä yksinkertaistamaan kääntämistä entisestään. Kirjoitan vain "cc ohjelma", jolloin source-hakemistossa oleva lähdekoodi ohjelma.c käännetään ja linkitetään valmiiksi ajettavaksi ohjelmaksi Progs-hakemistoon. Konekielipuo- lella käytössäni on niin ikään itse kirjoittamani Compile-ohjelma, joka ajaa en- sin makroassemblerin, joka kääntää source-hakemistossa (luonnollisesti pidän C- ja konekielikoodit eri hakemistoissa, nämä ovat c/source ja asm/source) olevan lähdekoodin object-hakemistoon objektikoodiksi, ja sen jälkeen linkkerin, joka edelleen linkkaa tämän objektikoodin ajettavaksi ohjelmaksi. Sekä konekieli- että C-ohjelman tapauksessa linkataan mukaan amiga.lib:ssä määriteltyjä arvoja, joihin tehdään ristiviittaus ohjelmassa. Lisäksi mukaan linkataan mahdollisesti muuta objektikoodia sekä rutiineja omista kirjastoistani. Käännöstyön ohjelmat Olen jo maininnutkin DICE:n. Se on Matthew Dillonin C-ympäristö, joka on eräs tämän hetken varteenotettavimmista Amigalla. Toinen suosittu paketti on gcc eli GNU C, mutta se on massiivinen niin olemukseltaan (kaikkiaan yli kahdeksan mega- tavua vieläpä pakattuna) kuin muistinkulutukseltaankin (4-8 megatavua). SAS C:n eli entisen Latticen tuki on lopetettu. Myös DICE on kaupallinen, mutta se ei ole ollenkaan niin hinnakas kuin SAS C oli. DICE maksaa $150 normaalille ihmi- selle ja $90 opiskelijalle. GNU C:n hyvä puoli on se, että se on ilmainen! DICE ja gcc ovat kattavia paketteja, ja niiden mukana tulee kaikki tarpeellinen oh- jelmisto ja kaikkea ylimääräistä vielä lisäksi. Myös makroassembleri ja linkkeri kuuluvat kauppaan. Konekieltä ohjelmoidessa kiinnitetään tietysti suurempi huomio makroassemble- riin. Aikansa hyvin palvelleet MC ja a68k on jo syytä unohtaa. Markkinoilla on monia hyviä moderneja kääntäjiä, jotka osaavat myös uusimpien prosessoreiden osoitusmuodot ja erikoisuudet. Itse olen varsin mieltynyt suomalaiseen SNMA:aan, joka on erittäin nopea ja tehokas. Assemblerin kanssa joutuu myös itse kiin- nittämään huomiota linkkeriin. Vanhat alink ja blink ovat historiaa. Itse olen käyttänyt varsin menestyksekkäästi DICE:n mukana tulevaa dlink:iä myös omien ko- nekieliohjelmieni kääntämiseen. Ainakaan SNMA:n tuottaman koodin kanssa dlin- killä ei ole ollut mitään ongelmia, kun taas blink kaatuili aika ajoin, samoin kuin MC. Dlink on hoidellut esimerkillisesti myös omat kirjastoni. Kääntäminen käytännössä C-kielinen ohjelma kääntyy näin: dcc source/prg.c -o Progs/prg -2.0 Välivaiheet eli konekielinen lähdekoodi ja objektikoodi kirjoitetaan tilapäisha- kemistoon, joka on normaalisti RAM-asemalla sijaitseva T:. Optio "-2.0" määrää kääntäjän käyttämään käyttöjärjestelmäversion 2.0 includeja. Konekielinen ohjelma prg kääntyy seuraavasti: SNMA source/prg.asm OBJ /object/prg.o INCLUDE /include dlink object/mystartup.o object/prg.o lib/amiga.lib -o Progs/prg SNMA haluaa välttämättä asettaa current diriksi sen hakemiston, jossa lähdekoodi on, joten object- ja include-hakemistoihin on viitattava taaksepäin. SNMA käyttää AmigaDOS-tyylistä komentorivitekniikkaa, kun taas dlink soveltaa Unix- tyylisiä "-"-merkillä alkavia yksimerkkisiä "avainsanoja". {3Ohjelmointi käytännössä {3----------------------- Tässä luvussa käsittelen yleisesti ohjelmointia Amigalla. Otan puheeksi joitakin asioita, jotka jokaisen ohjelmoijan tulee tietää. Ohjelman rakenne, C-ohjelma C-ohjelman pituus on heti ainakin viisi kilotavua. Se johtuu siitä, että ohjel- malle tehdään kaikkea pientä valmiiksi. C-ohjelma saa valmiina parsetun argu- menttijonon ja alustetut I/O-streamit stdin, stdout ja stderr. Alustuskoodi myös avaa kirjastoja valmiiksi. Ainakin DOS avataan aina, mutta esimerkiksi DICE osaa avata myös monet muut kirjastot riippuen siitä, mitä niistä ohjelmassa käytetään! Usein kuitenkin on parasta avata ne itse ohjelmassa, koska esimerkik- si koodi voidaan kääntää toisella kääntäjällä, sellaisella, joka ei automaatti- sesti avaa kirjastoja, jolloin tuloksena olisi ohjelma, joka ei avaa käyttämiään kirjastoja! Lisäksi startup-koodi alustaa DATA- ja BSS-alueet. Tämä tulee lähinnä kysymyk- seen residentattavan ohjelman kanssa, koska dos.libraryn LoadSeg() huolehtii jo niiden alustuksesta ladattaessa ohjelma DOS-asemalta. Residentattu ohjelma lada- taan muistiin kiinteästi, ja kaikki prosessit, jotka sitä ajavat, suorittavat samaa koodia muistissa. Tällöin data-alueet joudutaan alustamaan itse ohjelmas- sa, mutta C-kääntäjät osaavat ottaa sen huomioon. C-kääntäjät tarjoavat myös muita käteviä toimintoja, kuten pinon seurannan. Jos se on päällä, kun vapaan pinon määrä laskee liian pieneksi, ohjelmaan sisällytetty koodi varaa lisää pi- nomuistia! Ohjelman rakenne, konekieliohjelma Konekieliohjelmalle ei tehdä mitään valmiiksi, vaan se ajetaan aivan suoraan DO- Sista. Konekieliohjelma saa osoittimen argumenttijonoon A0:ssa ja sen pituuden D0:ssa. Useimmiten sen joutuu vieläpä itse päättämään komennolla clr.b -1(a0,d0.l). Jos ohjelma haluaa käyttää jotakin muuta kirjastoa kuin Execiä, on se avattava. Jos se haluaa tulostaa tai lukea tietoa esimerkiksi näppäimistöltä, on filehandle haettava itse. Konekieliohjelman tekeminen onkin huomattavasti hankalampaa kuin C-kielisen. Sen lisäksi, että konekielellä ohjelmointi on hitaampaa sen yksityiskohtaisemman luonteen takia, on tehtävä enemmän työtä. Tietysti on mahdollista kirjoittaa ge- neerinen oma startup-koodi käytettäväksi konekieliohjelman kanssa, jolloin työmäärä pienenee ratkaisevasti. Konekieli tulee kysymykseen erittäin pienien ohjelmien kanssa sekä C-kielisen ohjelman funktioiden kirjoituskielenä. Usein C-kieliset ohjelmat sisältävät konekielisiä osia. Jotkin osat on kätevää ja tar- koituksenmukaista kirjoittaa konekielellä C:n sijaan. Kaikkea ei C:llä voi tehdäkään. Esimerkiksi keskeytykset edellyttävät konekielen käyttöä, ja myös kirjastot on parasta kirjoittaa konekielellä. Yleistä ohjelmista Ohjelma saa käyttää kaikkia prosessorin rekisterejä, mutta pinoa ja pino-osoi- tinta ei saa sotkea. Ohjelman tulee palauttaa validi arvo (katso alla). Alioh- jelmat saavat käyttää rekisterejä D0/D1/A0/A1. Tämä pätee myös kirjastofunktioi- hin sekä C-ohjelman omiin konekielialiohjelmiin. Käyttäjä odottaa saavansa tietää ohjelmasta sen versionumeron. Tiedon saa Version-komennolla, mutta vain, jos se on sisällytetty ohjelmaan. Versiotieto annetaan erityisellä merkkijonol- la, joka alkaa "$VER:". C-kielellä: unsigned char versionstring[] = "$VER: Ohjelma 1.00"; Konekielellä: dc.b "$VER: Ohjelma 1.00",0 Nyt Version-komento osaa löytää versiotiedon ja tulostaa "Ohjelma 1.00". Ver- siostringin tulisi olla juuri tässä formaatissa eikä siinä saisi olla mitään ylimääräistä. Jotkut ohjelmoijat laittavat siihen itsekeskeisyydessään omia ni- miään, copyrighteja ja muuta siihen kuulumatonta roskaa, mikä voi olla harmitta- vaa. Lisäksi käyttäjä odottaa voivansa breikata ohjelman -C:llä. C-kääntäjä voi tarjota automaattisen breikkisysteeminkin, mutta mielestäni se kannattaa tehdä itse: if(SetSignal(0,0) & SIGBREAKF_CTRL_C) error("user break"); Tämän kun laittaa sellaiseen paikkaan, että se suoritetaan tarpeeksi usein, voi käyttäjä keskeyttää halutessaan ohjelman suorituksen. Esimerkkejä konekielellä Tässä on lyhyt konekieliohjelma: moveq #18,d0 addq.l #2,d0 rts Ohjelma ei tee muuta kuin laskee yhteen 18 ja 2 ja palauttaa arvon. Palautusarvo 20 vastaa FAIL-tilaa, joten ajettaessa tämä ohjelma saadaan ilmoitus sen "fai- laamisesta" eli jokin olisi mennyt vakavasti pieleen, jos kyseessä olisi ollut oikea ohjelma. Ohjelman tulee onnistuessaan palauttaa arvo 0. Tässä ovat tärkeimmät arvot: 0 OK Kaikki meni kuten suunniteltua 5 WARN Varoitus, jotain pientä meni vikaan 10 ERROR Virhe, kaikki ei onnistunut 20 FAIL Epäonnistuminen, kaikki meni päin seiniä Tässä on vähän järkevämpi ohjelma, mutta vain vähän: include "exec/types.i" ; yleisiä määrittelyjä include "exec/libraries.i" ; kirjastojen määrittelyjä SECTION prg_code,CODE movea.l 4,a6 ; Haetaan Execin kantaosoite movea.l a0,a2 ; Argumenttijonon osoitin move.l d0,d3 ; ja pituus talteen lea Dos(pc),a1 ; Osoitin dos.libraryn nimeen moveq #LIBRARY_VERSION,d0 ; kirjastoversio, mikä tahansa Lib OpenLibrary ; versio kelpaa meille tässä tst.l d0 ; testataan, saatiinko bne DosOK ; kirjasto auki moveq #20,d0 ; failataan, jos ei saatu rts DosOK movea.l d0,a6 ; DOSBase a6:een Lib Output ; Haetaan "stdout" move.l d0,d1 ; annetaan sen fh Writelle move.l a2,d2 ; argumenttijonon osoitin Lib Write ; pituus on valmiiksi d3:ssa movea.l a6,a1 ; DOSBase a1:een, josta movea.l 4,a6 ; kirjasto Lib CloseLibrary ; suljetaan clr.l d0 ; ja poistutaan, kaikki OK rts SECTION prg_data,DATA Dos dc.b "dos.library",0 END Ohjelma tulostaa sille annetun argumenttijonon, mutta ilman LF-koodia, joten prompti tulostuu sen perään. Ohjelma kaivannee selvennystä. A6 on rekisteri, jossa tulee olla aina kulloinkin kutsuttavan kirjaston kantaosoite, koska sitä kutsutaan sen kautta. Lisäksi kirjaston rutiinit saattavat myös hyödyntää sitä - Execin tapauksessa harvemmin, mutta varsinkin Intuition käyttää sitä usein. Kir- jastot käsitellään yksityiskohtaisesti myöhemmin. LIBRARY_VERSION on exec/libraries.h-includetiedostossa määritelty kiinteä muut- tuja, jonka arvo tällä hetkellä on 33. Se tarkoittaa vanhinta tuettua käyttöjärjestelmäversiota, ja suositus on avata kirjasto sillä, ellei tarvitse uudemman kirjaston ominaisuuksia. Kaikki merkkijonot tulee päättää nollalla. Output palauttaa filehandlen (fh) tulostuskanavaan, joka on normaalisti Shellin, josta ohjelma käynnistettiin, ikkuna. Se annetaan Writelle, kuin myös ohjelman parametreinä saamat argumenttijonon osoitin ja pituus. Tässä tapauksessa merkkijonoa ei tarvitse päättää nollaan, koska Write kirjoit- taa tietyn määrän merkkejä sen sijaan, että se tulostaisi niitä nollaan asti, kuten esimerkiksi C-kielen printf(). Pituus Writelle annetaan D3:ssa, johon se siirretään D0:sta jo heti ohjelman alussa. Tämä on yksi syy, miksi konekielellä ohjelmointi on C:tä tehokkaampaa. Siinä on mahdollista tehdä tuollaisia ennakoi- via toimenpiteitä, jotka lyhentävät ohjelmaa. C-kääntäjän tuottamassa koodissa pituus olisi kirjoitettu ties minne ja sitten vasta D3:een, kun sitä olisi siellä tarvittu, mutta me osaamme laittaa sen sinne jo valmiiksi. Ohjelmassa käytetty Lib on makro, joka kutsuu kirjastofunktiota: Lib MACRO * routinename[,basereg] xref _LVO\1 ; _LVO#? eli offset-arvo haetaan ifnc '\2','' ; jostain muualta eli amiga.lib:stä movea.l \2,a6 ; jos basereg on määritelty, siirretään endc ; base sieltä A6:een, kuten kuuluu jsr _LVO\1(a6) ; ja kutsutaan itse funktiota ENDM LVO eli Library Offset Vector on arvo, jonka mukaan funktiota kutsutaan. Writen tapauksessa offsetin nimi on _LVOWrite. Se asetetaan xref:llä ulkoiseksi muuttu- jaksi. Linkkeri täyttää muuttujan arvon hakemalla sen amiga.lib:stä. Ohjelmassa tarkistetaan OpenLibraryn palauttama arvo, joka on kirjaston kantaosoite tai nolla, jos kirjasto ei jostain syystä auennut, jolloin emme voi jatkaa ja ohjel- man tulee failata. Kaikki palautusarvot pitää aina tarkistaa. Jos funktio vain voi failata, on mahdollinen virhekoodi löydettävä, koska useimmiten virheen jälkeen ei voida tehdä sitä, mitä pitäisi ja sen yrittäminen ehkä kaataa koneen tai ainakin saa aikaan suuria vaikeuksia. Esimerkkejä C-kielellä Tässä tulee äskeistä konekieliohjelmaa pidemmälle viety C-kielinen ohjelma: #include "proto/exec_protos.h" #include "exec/types.h" #include "dos/dos.h" main(ac,char*av[]); main(ac,char*av[]) { printf("Ensimmäinen parametri on \"%s\".\n",av[1]); return(RETURN_OK); }; Ohjelma on huomattavasti yksinkertaisempi, vaikka se tulostaa jopa johdannon ja rivinvaihdon sekä nimenomaisesti ensimmäisen parametrin. RETURN_OK vastaa arvoa 0. Paluuarvojen määrittelyt ovat dos.h-tiedostossa. Itse asiassa edellisessä ko- nekieliohjelmassakin olisi pitänyt failauskohdassa olla RETURN_FAIL. Ensimmäinen include, "exec_protos.h", sisältää prototyypit kaikille Execin funktioille. Se on syytä aina ladata, samoin kuin muidenkin käytettävien kirjastojen prototyy- pit, jottei tulisi hankaluuksia. Tässä tulee uusi versio: #include "proto/exec_protos.h" #include "proto/dos_protos.h" #include "exec/types.h" #include "dos/dos.h" _main(void); unsigned char *as; _main(void) { as=GetArgStr(); Write(Output(),as,strlen(as)); return(RETURN_OK); }; Nyt käytän _main()-entrypointia. Normaalisti _main():ksi tulee DICE:n oma _main()-funktio, mutta nyt korvaan sen omallani. Varsinainen _main() tekee oh- jelman alustukset, joita olen jo muutamaan kertaan luetellut, minkä jälkeen se kutsuu main()-funktiota, joka on normaalisti ohjelman pääfunktio. Nyt se kuiten- kin on _main(). Hyöty on se, että tässä tapauksessa tarpeeton alustus jää pois ja ohjelman pituudeksi tuli vain 616 tavua. Tällöin kuitenkin printf() on käyttökelvoton, samoin argc ja argv, mutta yllä kuvatuilla tavoilla ne voi kiertää. Tosin argumenttijonon parseamisen joutuu tekemään itse, mutta usein se itse tehdäänkin. Tämä ei kuitenkaan ole suotava tapa toimia. DICE on hyvin riip- puvainen omasta alustuskoodistaan, joten sitä ei pidä mennä jättämään pois, jol- lei tiedä varmasti, mitä tekee. Tähän loppuu artikkelin yleinen ohjelmointiosuus. Uusia asioita ohjelmien teke- miseen liittyen voi tulla vielä esille, mutta nyt aletaan käydä läpi Execin osia yksi kerrallaan. Jokainen osa kuvataan yksityiskohtaisesti ja esimerkkikoodia tulee, tästä lähtien kaikki C-kielellä, joka on *se* kieli, jolla ohjelmat tulee kirjoittaa, paitsi erityisesti konekielistä koodia edellyttävissä tilanteissa. Mukana on myös edistyneempää ohjelmointia. {3Signaalit {3--------- Signaalit ovat Execin tarjoama keino välittää yksinkertaista tietoa esimerkiksi tehtävien välillä. Signaalit ovat tehtävienvälisen kommunikaation kaikkein alin taso, jonka varassa lepäävät kaikki muut järjestelmät. Useat signaalit voivat olla aktiivisia yhtä aikaa. Jokaisella tehtävällä on 32 signaalibittiä, joista 16 on varattu käyttöjärjestelmän käyttöön. Loput 16 ovat vapaasti ohjelmoijan käytettävissä. Harvemmin niitä kuitenkin tarvitsee suoraan itse käyttää, joten voit hypätä tämän kappaleen yli. Pääasia on, että tiedät, mitä signaalibitit ovat, koska et todennäköisesti tule koskaan tarvitsemaan niiden suoraa hyödyntämistä. Viestiportin käyttöön voidaan signaalibitti varata automaattisesti, mutta jos sitä tarvitaan johonkin muuhun tarkoitukseen, se tehdään näin: UBYTE signal; if((signal = AllocSignal(-1)) < 0 ) printf("Ei vapaita signaalibittejä"); else { printf("Varatun signaalibitin numero on %ld.\n",signal); FreeSignal(signal); } Signaalia odotetaan Wait()-funktiolla. Sille ei kuitenkaan anneta signaalibitin numeroa vaan maski, joka muodostetaan kaikista odotettavista signaalibiteistä. Wait() odottaa, kunnes yksi signaaleista saadaan ja palauttaa maskin, josta on saatavissa selville aktivoitunut signaali. Usea signaali voi aktivoitua samalla kertaa, joten kaikki odotetut signaalit tulee tutkia: firstsigmask = 1L << firstsigbit; secondsigmask = 1L << secondsigbit; signalmask = Wait(firstsigmask | secondsignalmask | SIGBREAKF_CTRL_C); if(signals & firstsigmask) printf("Ensimmäinen signaali tuli aktiiviseksi\n"); if(signals & secondsigmask) printf("Toinen signaali tuli aktiiviseksi\n"); if(signals & SIBREAKF_CTRL_C) printf("User break\n"); Tässä odotamme kahta signaalia, joiden bittinumerot ovat muuttujissa firstsigbit ja secondsigbit. Niistä muodostetaan maskit, jotka yhdistetään ja annetaan Wait():lle, joka sitten aikanaan palauttaa maskin, jossa ovat päällä kaikki ak- tiiviset signaalit. Tämän jälkeen ne kaikki tutkitaan yksitellen. Mukana on vielä SIGBREAKF_CTRL_C, joka on yksi käyttöjärjestelmälle kuuluvista kiinteistä signaalibiteistä. Tehtävä saa tällaisen signaalin, kun käyttäjä painaa -C:tä. Signaalibittejä voi itse manipuloida funktiolla SetSignal(). Sen käyttämiseen on kiinnitettävä erityistä huomiota, koska se suorittaa erittäin matalan tason toi- mintoja. SetSignal():lle annetaan kaksi parametriä, signaalibittien ja maskin uudet arvot. Maski on luku, jossa ovat ne signaaleja vastaavat bitit päällä, joiden arvo asetetaan arvoluvun vastaavien bittien mukaisesti. Tämä ei ole eri- tyisen yksinkertainen asia, joten tässä on muutama esimerkki: SetSignal(0,-1) nollaa kaikki signaalit SetSignal(6,3) nollaa nollabitin ja asettaa ykkösbitin SetSignal(0,0) ei muuta mitään, vaan ainoastaan palauttaa signaalit Näistä viimeistä voidaan käyttää signaalien tilan tarkistamiseen. Se ei nollaa mahdollista aktiivista signaalibittiä, kuten Wait(), joten se on tehtävä käsin signaalin saamisen jälkeen. Parhaiten SetSignal():n toimintaa selventänee kes- kimmäinen esimerkki. Arvosta 6 ei käytetä ollenkaan bittiä 2, arvoltaan 4, koska maskissa ovat päällä bitit 0 ja 1, joten vain ne asetetaan arvon mukaisesti. Ar- vossa bitti 0 on pois päältä ja bitti 1 päällä, joten myös vastaavat signaalit saavat nämä tilat. Exec tarjoaa viisi funktiota signaalien hyödyntämiseen: ULONG SetSignal( unsigned long newSignals, unsigned long signalSet ); Asettaa ja/tai tarkistaa halutut signaalibitit. Tästä olikin jo esimerkki aikai- semmin -C:n tunnistamisessa. Tällä funktiolla voidaan tutkia tehtävän sig- naalien tilaa antamalla kummaksikin parametriksi nolla, mutta yleensä signaaleja tulisi odottaa Wait()-funktiolla. ULONG Wait( unsigned long signalSet ); Odottaa haluttuja signaalibittejä ja palauttaa aktiiviset signaalit ja nollaa kaikki aktivoituneet signaalit. Tästä syystä on ehdottomasti tarkistettava kaik- ki palautetut bitit, jotta signaali ei pääsisi livahtamaan ohi huomaamatta, kos- ka seuraavalla kerralla se ei enää ole päällä. Wait(0) aiheuttaisi loppumattoman odottamisen. void Signal( struct Task *task, unsigned long signalSet ); Signaloi toista tehtävää. Signaalibitti tulee tällöin signaloitavan tehtävän signaaleista. Tällaista toimintatapaa ei voi hyödyntää tietämättä, mitä signaa- lia toinen tehtävä odottaa. Kyseeseen tuleekin lähinnä ohjelman alatehtävä, joka on varannut signaalibitin ja kertonut päätehtävälle sen numeron, jotta se osaa aktivoida oikean signaalin. BYTE AllocSignal( long signalNum ); Varaa signaalin käyttöä varten. Numero voi olla -1, jolloin varataan seuraava vapaana oleva. Signaalit eivät ole globaaleja, vaan jokaisella tehtävällä on oma joukkonsa signaaleita. void FreeSignal( long signalNum ); Vapauttaa varatun signaalin. {3Listat ja jonot {3--------------- Amigan ympäristön olennainen ominaisuus on dynaamisuus. Myös järjestelmän da- tastruktuurit ovat dynaamisia rakenteeltaan. Järjestelmä on joustava eikä rajoja juuri ole. Dataa tallennetaan dynaamisesti luotaviin struktuureihin (joukko tie- toa ennalta määrätyssä muodossa), joita pidetään listoissa. Lista voi olla tyhjä, mutta ei koskaan täysi. Lista voi myös olla järjestetty, jolloin sitä kutsutaan jonoksi (queue). Käytän siitä hämäännyksen vähentämiseksi vastaisuu- dessa englanninkielistä nimeä. Exec pitää kaiken järjestelmään liittyvän tiedon listoissa. Lista koostuu heade- rista ja kaksoislinkatusta ketjusta elementtejä, joita kutsutaan nodeiksi. Hea- der sisältää osoittimen listan ensimmäiseen ja viimeiseen nodeen. Header toimii handlena koko listaan. Listoihin voidaan liittää nodeja ja niitä voidaan poistaa niistä. Listaa käsitellessä ei tarvitse tietää, millaista tietoa se sisältää. Exec tarjoaa listojen käsittelyyn joukon funktioita, joita voidaan käyttää kaik- kien listojen kanssa. Node on ryhmä toisiinsa liittyvää tietoa, joka kuvaa jonkin asian. Itse asiassa nodet ovat erilaisia struktuureja, jotka alkavat Node-struktuurilla, jonka avul- la listaa ylläpidetään. Nodet voivat sijaita missä tahansa muistissa toisistaan riippumatta. Ne pitävät kiinni toisistaan kahden osoittimen avulla. Kaksoislin- kattu lista tarkoittaa sitä, että jokaisessa nodessa on osoitin sitä edeltävään ja seuraavaan nodeen listassa. Näitä kutsutaan nimillä predecessor ja successor. Listan ensimmäisen noden eli Head-noden edeltäjä on listan header. Vastaavasti listan viimeisen noden eli Tail-noden jäljittäjä on niin ikään listan header. Kuten sanottu, headerissa on osoittimet listan Head- ja Tail-nodeen. Tyhjässä listassa nämä osoittavat toisiinsa. Listasta on olemassa myös supistettu versio, MinList. Nodejen määrittelyt tulevat tässä: struct Node { struct Node *ln_Succ; /* Osoitin seuraavaan nodeen (successor) */ struct Node *ln_Pred; /* Osoitin edelliseen nodeen (predecessor) */ UBYTE ln_Type; /* Noden tyyppi, sama kuin listan tyyppi */ BYTE ln_Pri; /* Prioriteetti, listan järjestämistä varten */ char *ln_Name; /* ID-stringi, päättyy nollaan */ }; struct MinNode { struct MinNode *mln_Succ; struct MinNode *mln_Pred; }; Tyyppejä on monia. Listassa voi olla vain keskenään samantyyppisiä nodeja. Lista voidaan järjestää nodejen prioriteettien mukaiseen järjestykseen. Tällöin sitä kutsutaan jonoksi eli queueksi. Nimeä harvemmin käytetään. Yleisimmät nodejen tyypit ovat NT_TASK, NT_INTERRUPT, NT_MSGPORT ja NT_MESSAGE. Yksi esimerkki ni- men hyödyntämisestä on kirjasto. Kirjastot alkavat nodella, jolloin nimi on kir- jaston nimi, esim. exec.library. Tällöin noden tyyppi on vastaavasti NT_LIBRARY. Exec pitää kirjastot kirjastolistassa, josta ne voidaan helposti löytää. Tässä tulevat headerien määrittelyt: struct List { struct Node *lh_Head; /* Head-node */ struct Node *lh_Tail; /* Nolla */ struct Node *lh_TailPred; /* Tail-node */ UBYTE lh_Type; UBYTE l_pad; }; struct MinList { struct MinNode *mlh_Head; struct MinNode *mlh_Tail; struct MinNode *mlh_TailPred; }; Minimaalinen lista käy yksiin täyden listan alun kanssa, mutta sen tyyppiä ei voida testata. Tässä on kaavio, joka selventää headerin rakennetta: Head-node Tail-node Headeri ln_Succ lh_Head ln_Pred = 0 ln_Succ = 0 lh_Tail = 0 ln_Pred lh_TailPred Header on siis tavallaan listan Head- ja Tail-nodejen yhteensulautuma, kun nii- den ajatellaan menevän lomittain päällekkäin. Header alustetaan kuvaamaan tyhjää listaa asettamalla lh_Head osoittamaan lh_Tailiin ja lh_TailPred lh_Headiin sekä nollaamalla lh_Tail. Myös tyyppi tulee asettaa oikeaksi, jos käytetään täyttä headeria. Amiga.lib sisältää rutiinin listan alustamiseen - C:llä NewList() ja konekielellä NEWLIST, joka on lists.i-tiedostossa määritelty makro. Tässä on kuitenkin vastaava koodi kummallakin kielellä: /* c */ struct List list; list.lh_Head = (struct Node *) &list.lh_Tail; list.lh_Tail = 0; list.lh_TailPred = (struct Node *) &list.lh_Head; ; assembly (a0 = osoitin alustettavaan headeriin) move.l a0,LH_HEAD(a0) addq.l #4,LH_HEAD(a0) clr.l LH_TAIL(a0) move.l a0,LH_TAILPRED(a0) Listan tyhjyyden voi tarkistaa. Siihen on monia tapoja, mutta tässä on eräs kum- mallakin kielellä: /* c */ if(list->lh_TailPred == (struct Node *)list) printf("Lista on tyhjä\n"); ; assembly cmp.l LH_TAILPRED(a0),a0 beq List_is_empty Lista skannataan helposti ottamalla aina seuraavan noden osoitin: struct List *list; struct Node *node; for(node = list->lh_Head; node->ln_Succ; node = node->ln_Succ) printf("Node nimeltä %s on osoitteessa %lx.\n",node->ln_Name,node); Tässä ovat Execin listojen käsittelyyn tarjoamat funktiot: void Insert( struct List *list, struct Node *node, struct Node *pred ); Lisää noden listaan haluttuun paikkaan. Tässä list on tietysti lista, johon lisätään, ja node on node, joka lisätään. Lisäksi pred on osoitin nodeen, jonka jälkeen listassa uusi node sijoitetaan. Uuden noden predecessor osoittaa lisäämisen jälkeen siihen ja successor siihen, joka ennen lisäämistä oli sen perässä listassa. Insert():llä voidaan lisätä node myös listan alkuun tai lop- puun, mutta se ei ole tehokasta. Siihen kannattaa käyttää alla olevia erityis- funktioita, ja Insert():iä vain silloin, kun node sijoitetaan tiettyyn paikkaan keskelle listaa. void AddHead( struct List *list, struct Node *node ); Lisää noden listan alkuun eli siitä tulee sen Head-node. Lisäämisen jälkeen hea- derin lh_Head osoittaa tähän nodeen. void AddTail( struct List *list, struct Node *node ); Lisää noden listan loppuun eli siitä tulee sen Tail-node. Lisäämisen jälkeen headerin lh_TailPred osoittaa tähän nodeen. void Remove( struct Node *node ); Poistaa noden listasta. Poistamisen jälkeen noden successor ja predecessor ovat invalideja. struct Node *RemHead( struct List *list ); Poistaa listan Head-noden. struct Node *RemTail( struct List *list ); Poistaa listan Tail-noden. void Enqueue( struct List *list, struct Node *node ); Lisää noden listaan kuten Insert(), mutta sen paikka listassa määräytyy priori- teettien mukaan. Listan ensimmäinen node on suuriprioriteettisin, ja prioriteet- ti laskee loppua kohden. Samanprioriteettiset nodet laitetaan listaan FIFO-pe- riaatteella eli node lisätään viimeisen samanprioriteettisen noden perään. Lis- ta, johon nodet lisätään Enqueue():lla on jono eli queue. struct Node *FindName( struct List *list, UBYTE *name ); Etsii listasta halutunnimisen noden ja palauttaa osoittimen ensimmäiseen nodeen, jonka nimi täsmää annetun merkkijonon kanssa. Erikoisuutena on se, että funktion avulla voidaan etsiä kaikki listassa olevat tietynnimiset nodet, vaikka niitä olisi useita. Tällöin headerin sijaan funktiolle annetaankin sen palauttaman no- den osoitin, jolloin se jatkaa etsimistä eteenpäin listassa ja palauttaa mahdol- lisesti seuraavan täsmäävän noden: struct List *list; struct Node *node; unsigned char name[]; if(node = FindName(list,name)) while(node) { printf("Node nimeltä %s löytyi osoitteesta %lx.\n",node->ln_Name,node); node = FindName((struct List *)node,name); } else printf("Nodea ei löytynyt nimellä %s.\n",name); {3Portit ja viestit {3----------------- Seuraava askel syvemmälle Execin toimintaan on "interprocess communication" eli tehtävienvälinen kommunikaatio. Tästä olikin puhetta jo kurssin ensimmäisessä osassa. Nyt katsotaan, miten tieto siirtyy ohjelmien välillä käytännössä. Ideana on, että tehtävät lähettävät viestejä toisilleen. Viestit ovat samoja dy- naamisesti varattavia struktuureja, joista oli puhetta edellisessä luvussa. Myös keskeytys voi lähettää viestin tehtävälle tai päin vastoin. Viesti on kaksiosai- nen. Ensimmäinen osa on vakio kaikilla viesteillä ja pitää sisällään tietoa viestistä, mm. sen koon. Edelleen, se alkaa nodella, joihin tutustuttiin edelli- sessä luvussa. Toinen osa on viestin sisältö, data, joka siirretään. Sitä voi olla jopa melkein 64 kilotavua. Viesti lähetetään aina kohdeporttiin. Siihen kuuluu viestilista, johon saapuvat viestit niiden noden avulla lisätään. Kun viesti otetaan vastaan, se poistetaan portin listasta. Myös porttistruktuuriin kuuluu node. Sen avulla Exec pitää por- tit listassa. Näin tietty portti voidaan helposti etsiä edellisessä luvussa ku- vatuin keinoin. Viestiä ei kopioida, vaan se sijaitsee staattisesti muistissa. Käytännössä vain osoitin viestiin siirretään, mutta teoriassa katsotaan myös käyttöoikeus vies- tistruktuuriin siirtyväksi viestin vastaanottajalle. Kun viesti on lähetetty, lähettäjä ei saa koskea siihen, ennen kuin saa viestin takaisin vastauksena, minkä jälkeen se taas kuuluu lähettäjälle. Kun viesti saapuu viestiporttiin, se lisätään sen viestilistan perään. Kun vies- ti vastaanotetaan portista, se otetaan listan alusta. Näin viestit saadaan aina saapumisjärjestyksessä. Viestin saapuminen viestiporttiin voi aikaansaada sig- naalin sen omistajalle tai aiheuttaa ohjelmallisen keskeytyksen. Tässä tulevat määrittelyt viestiportille ja viestille: struct MsgPort { struct Node mp_Node; /* Node */ UBYTE mp_Flags; /* liput */ UBYTE mp_SigBit; /* signaalibitin numero */ void *mp_SigTask; /* kohde, jota signaloidaan */ struct List mp_MsgList; /* viestilista */ }; struct Message { struct Node mn_Node; /* Node */ struct MsgPort *mn_ReplyPort; /* vastausportti */ UWORD mn_Length; /* viestin pituus */ }; SigTask sisältää osoittimeen ohjelman, jota signaloidaan, Task-struktuuriin. Mikäli tarkoitus on aiheuttaa keskeytys, se onkin osoitin Interrupt-struktuu- riin. Nämä käsitellään myöhemmin. ReplyPort on osoitin lähettäjän omaan viesti- porttiin. Viesti linkataan tämän portin viestilistaan, kun vastaanottaja vastaa siihen. Pituus on Message-struktuurin pituus plus datablokin koko. Viestiportin voi luoda varaamalla tarpeeksi muistia ja alustamalla sen kentät. Viestilista tulee ehdottomasti alustaa NewList()-funktiolla tai NEWLIST-makrol- la. V36 tarjoaa myös funktion CreateMsgPort(), jolla portin voi luoda automaat- tisesti. Tämän jälkeen portin voi tehdä julkiseksi, mutta usein se ei ole tar- peen. Julkinen portista tulee silloin, kun se lisätään Execin porttilistaan, josta toiset tehtävät voivat saada osoittimen siihen ja lähettää tehtävälle viestejä. V36:lla portti tehdään ja tuhotaan näin: struct MsgPort *mp; if(mp = CreateMsgPort()) { mp->mp_Node.ln_Name = "Portin nimi"; /* tarpeen vain julkisissa */ mp->mp_Node.ln_Pri = 2; /* erityisen tärkeä portti */ AddPort(mp); /* lisätään portti listaan */ /* Portin käyttöä */ RemPort(mp); /* poistetaan portti listasta */ DeleteMsgPort(mp); /* ja tuhotaan se */ } else printf("Porttia ei saatu tehtyä.\n"); Nimeä ja prioriteettia tarvitaan vain, jos portista tehdään julkinen eli se lisätään Execin porttilistaan. Porttilistaan lisätty portti pitää aina muistaa myös poistaa ennen sen tuhoamista. Tällaiseen porttiin lähetetään toisesta oh- jelmasta viesti näin: struct MsgPort *port; struct Message *msg; if(port = FindPort("Portin nimi")) PutMsg(port,msg); Portin löytyminen täytyy aina tarkistaa, eikä antaa FindPort():n palauttamaa osoitinta suoraan PutMsg():lle. Nyt siirrytään taas vastaanottavaan ohjelmaan. Viesti otetaan vastaan ja siihen vastataan seuraavasti: struct MsgPort *mp; struct Message *msg; while(!(port = GetMsg(mp))) WaitPort(mp); /* viestin käsittelyä */ ReplyMsg(msg); Tässä viestiä odotetaan WaitPort()-funktiolla. Mikäli tarvitsee odottaa jotakin muutakin, joudutaan käyttämään Wait()-funktiota, kuten yleensä on asian laita. Tällöin kyseeseen tuleva toimintatapa on selvitetty aiemmin tässä artikkelissa, mutta tässä on vielä esimerkki, jossa odotetaan viestiä ja breikkiä: portsigmask = 1L << mp->mp_SigBit; Wait(portsigmask | SIGBREAKF_CTRL_C); Lopuksi tässä ovat vielä Execin viesteihin liittyvät funktiot: void AddPort( struct MsgPort *port ); Lisää viestiportin porttilistaan tehden siitä julkisen, jolloin mikä tahansa järjestelmässä ajettava ohjelma voi etsiä sen ja lähettää siihen viestin. void RemPort( struct MsgPort *port ); Poistaa julkisen portin Execin porttilistasta. Tämä on toimenpide, joka on muis- tettava aina tehdä ennen portin tuhoamista tai poistumista ohjelmasta. void PutMsg( struct MsgPort *port, struct Message *message ); Lähettää viestin eli laittaa sen kohdeporttiin. struct Message *GetMsg( struct MsgPort *port ); Vastaanottaa viestin portista. void ReplyMsg( struct Message *message ); Vastaa viestiin eli lähettää vastaanotetun viestin takaisin lähettäjälle. struct Message *WaitPort( struct MsgPort *port ); Odottaa viestiä saapuvaksi porttiin ja palaa vasta sitten, kun sellainen tulee. Yleensä on kuitenkin tarve odottaa useita signaaleja, jolloin joudutaan käyttämään WaitPort():n sijasta Wait():ia, jolle on silloin rakennettava maski portin signaalibitistä. struct MsgPort *FindPort( UBYTE *name ); Etsii porttia porttilistasta ja palauttaa osoittimen porttiin, jonka nimi täsmää annettuun merkkijonoon, tai nollan, jos halutunnimistä porttia ei ollut listal- la. {3Kirjastot {3--------- Moneen otteeseen on jo ollut puhetta kirjastoista. Ne ovat funktiokasautumia, jotka liittyvät tiettyyn osa-alueeseen. Kirjastojen funktioita kutsutaan käyttäen niiden kantaosoitetta, joka saadaan avaamalla kirjastot. Käytettävät kirjastot on aina avattava ja lopuksi suljettava. DICE osaa avata tärkeimmät kirjastot automaattisesti, kun se havaitsee viittauksen niiden kantaosoitteen nimeen. Tässä on tärkeimpien kirjastojen osoittimien (library base pointer) nimiä: asl.library AslBase commodities.library CxBase dos.library DOSBase exec.library SysBase expansion.library ExpansionBase graphics.library GfxBase icon.library IconBase iffparse.library IFFParseBase intuition.library IntuitionBase layers.library LayersBase utility.library UtilityBase version.library workbench.library WorkbenchBase Kirjasto koostuu funktioiden hyppykäskytaulukosta ja kirjastostruktuurista: Kantaosoite + sizeof Lib kirjastokohtaista tietoa Kantaosoite struct Library Kantaosoite - 6 JMP Func1 Kantaosoite - 12 JMP Func2 Kantaosoite - 18 JMP Func3 ... Kantaosoitteessa on siis kirjaston node, Library-struktuuri, jonka perässä yleensä tulee lisää kirjastokohtaista tietoa. Hyppytaulukko, kuten jo mainittu, on kantaosoitteesta taaksepäin. Ensimmäiseen funktioon hyppäävä käsky on -6:ssa, joka on sen LVO eli vektorioffset. Funktiota kutsutaan hyppäämällä tähän osoit- teeseen: jsr -6(a6) Kantaosoitteen tulee olla a6:ssa. Ensimmäiset neljä funktiota on varattu: LVO Funktio Tarkoitus -6 Open Avaa kirjaston (OpenLibrary() kutsuu) -12 Close Sulkee kirjaston (CloseLibrary() kutsuu) -18 Expunge Poistaa kirjaston muistista, jos ei käyttäjiä -24 Res. Ei toimintoa, varattu paikka, palauttaa nollan Näitä funktioita ohjelman ei tavallisesti tarvitse koskaan kutsua. Exec kutsuu niitä (lisää tietoa myöhemmin). Ensimmäinen varsinainen funktio on offsetissä -30, seuraava on -36 ja niin edelleen. C-kielellä ohjelmoitaessa näistä asioista ei tarvitse huolehtia ollenkaan. Kirjasto avataan näin: struct Library *IntuitionBase; if(!(IntuitionBase = OpenLibrary("intuition.library",LIBRARY_VERSION))) { printf("Intuition ei auennut\n"); exit(RETURN_FAIL); } Rakenteellisesti parempi tapa olisi: if(IntuitionBase = OpenLibrary("intuition.library",LIBRARY_VERSION)) { ...... } else printf("Intuition ei auennut\n"); Intuition on siinä määrin keskeinen kirjasto, että jos se ei aukea, on järjes- telmä jo aika nurin. Kirjasto suljetaan lopuksi näin: CloseLibrary(IntuitionBase); Jos kutsut cleanup-koodia myös virhetilanteissa, on tarkistus tarpeen: if(IntuitionBase) CloseLibrary(IntuitionBase); Jos et ole kiinnostunut kirjastojen tekemisestä, katso luvun lopusta kirjastojen käyttöön liittyvien funktioiden selitykset ja hyppää seuraavaan lukuun. Kirjastojen tekeminen Jos olet kirjoittanut joukon funktioita, joita käytetään useissa ohjelmissa, kannattaa harkita niiden sijoittamista kirjastoon. Kerron nyt lyhyesti, miten kirjasto tehdään. Ensin katsotaan kantaosoitteessa olevaa struktuuria: struct Library { struct Node lib_Node; UBYTE lib_Flags; UBYTE lib_pad; UWORD lib_NegSize; /* Kirjaston koko alas päin */ UWORD lib_PosSize; /* Kirjaston koko ylös päin */ UWORD lib_Version; /* versionumero */ UWORD lib_Revision; /* merkityksettömämpi revisionumero */ APTR lib_IdString; /* Kirjaston tunniste */ ULONG lib_Sum; /* Kirjaston tarkistussumma */ UWORD lib_OpenCnt; /* Käyttäjien määrä */ }; Tätä kirjastonodea ei sisällytetä segmenttiin, vaan se tehdään lennossa kirjas- toa ladattaessa. Tiedot toki saadaan kirjastotiedostosta. IdString on kirjaston tunniste, joka on muodossa "nimi versio.revisio (päiväys)". Lopussa tulee olla ensin CR ja sitten LF. Merkkijonon tulee päättyä nollatavuun. Tässä on esimerkki kirjaston idauksesta: graphics 40.8 (15.3.93) Tässä tapauksessa kirjaston versionumero on 40 (lib_Version) ja revisio 8 (lib_Revision). Exec ylläpitää kirjastosta tarkistussummaa (lib_Sum). Kirjaston koko pidetään kahdessa muuttujassa, erikseen koko kantaosoitteesta alaspäin (hyppytaulukko) ja ylöspäin (datastruktuuri). Kirjaston koodi on eri paikassa. Kirjasto on aina tehtävä konekielellä - ainakin sen runko. Tässä on lähtökohta siihen: STRUCTURE RT,0 UWORD RT_MATCHWORD ; tunniste APTR RT_MATCHTAG ; osoitin siihen... APTR RT_ENDSKIP ; lopun osoitin UBYTE RT_FLAGS ; lippuja UBYTE RT_VERSION ; versionumero UBYTE RT_TYPE ; moduulin tyyppi (kirjasto) BYTE RT_PRI ; prioriteetti APTR RT_NAME ; osoitin moduulin nimeen APTR RT_IDSTRING ; osoitin moduulin idaukseen APTR RT_INIT ; alustusosoitin LABEL RT_SIZE Tämä Resident-struktuuri määrittää moduulin. Moduuli on tässä tapauksessa kir- jasto, mutta se voi olla muukin, esimerkiksi laiteohjain. RomTag (RT) alkaa matchwordilla, jonka arvon tulee olla $4AFC. Siitä se tunnistetaan. Seuraavaksi tulee osoitin tuohon sanaan ja sitten osoitin, jota käytetään lähinnä moduulien skannaukseen ROMissa. Sen tulisi osoittaa koodin loppuun. Muut kentät ovat itse- kuvaavia. Liput ovat yleensä RTF_AUTOINIT. Tämä on erityinen alustusmoodi. Jos AUTOINIT- lippu ei ole päällä, Exec ei alusta kirjastoasi automaattisesti. Sen sijaan kut- sutaan funktiota, jonka osoitin on RT_INIT:ssä. Tämän funktion tulee tehdä myöhemmin kerrottavat alustustoimenpiteet. Kannattaa kuitenkin hyödyntää AUTOI- NIT-toimintoa. Kun se on päällä, onkin RT_INIT:ssä osoitin taulukkoon: dataSize Kirjaston data-alueen koko vectors Osoitin funktiotaulukkoon structure Osoitin InitStruct()-dataan initFunction Osoitin alustusfunktioon Data-alueen koko käsittää Library-struktuurin sekä mahdolliset omat lisät. Funk- tiotaulukko koostuu joko longword-osoitteista kirjaston funktioihin, tai en- simmäisen wordin ollessa -1, word-offseteista funktioiden alkuun suhteellisina taulukon alkuun. Jälkimmäinen tapa voi olla hieman hankalampi implementoida, mutta se on tehokkaampi. Exec kutsuu myös InitStruct()-funktiota, jolla alustetaan Library-node ja muut mahdolliset omat datat. Alustusfunktiota kutsutaan sen jälkeen, kun kirjasto on kunnossa. Sillä ei ole enää paljon tehtävää, sille jää lähinnä vain kirjanpidol- lisia tehtäviä sekä tietysti tarvittaessa alustettavat kirjaston omat datat. Aikaisemmin mainitut neljä ensimmäistä funktiota ovat pakollisia jokaisessa kir- jastossa. Open():n tehtävä on lähinnä kasvattaa opencountia ja varata avaajalle käyttäjäkohtaisia data-alueita, jos niille on tarvetta tms. Close() vähentää opencountia ja vapauttaa varaukset ym. Expunge() poistaa kirjaston muistista. Sitä varten pitää vapauttaa muistialueet ja poistaa kirjaston segmentit muistis- ta. Reserved()-funktio on myös pakko implementoida. Sen tulee palauttaa nolla. Olen nähnyt kirjaston lähdekoodin, jossa Reserved()-funktion paikalle hyppytaulukkoon laitettiin nolla. Tuloksena on varmuudella koneen kaatuminen tai muu häiriöti- lanne, kun tälle funktiolle keksitään käyttöä, ja tulevaisuuden käyttöjärjes- telmä kutsuu sitä - ja tällaisilla kirjastoilla hyppää tuohon osoitteeseen. Kirjaston tekeminen ei välttämättä ole helppoa, joten laitan tähän hieman koodia esimerkiksi. Tämä tulee suoraan sh.libraryn lähdekoodista. Poistin tosin funk- tiomäärittelyt - laitoin vain muutaman esimerkiksi, jotta ne eivät sotkisi tässä. Segmentti alkaa pienellä koodinpätkällä, jonka tarkoitus on estää kirjas- ton ajaminen ohjelmana. RTC_MATCHWORD on laiton käsky, jolloin sen ollessa tie- doston alussa kone vain kaatuisi nätisti, mutta pienellä ylimääräisellä koo- dinpätkällä sekin voidaan välttää. VER equ 10 ; versionumero REV equ 297 ; revisionumero start moveq #-1,d0 rts RTAG DC.W RTC_MATCHWORD DC.L RTAG,FINAL DC.B RTF_AUTOINIT,VER,NT_LIBRARY,0 DC.L LNAME,IDSTR,INITD INITD DC.L sh_base_sizeof,funcs,datat,initc funcs dc.w -1,open-funcs,close-funcs,expunge-funcs,extfunc-funcs dc.w func1-funcs,func2-funcs,-1 ; Tämä on InitStruct():n datataulukko. INIT#?-makroilla tehdään sille ; "komentojono", jolla alustetaan noden kentät. Lisää tietoa InitStruct():n ; toiminnasta on include-tiedostossa "exec/initializers.i". Se käsitellään ; ehkä joskus. datat INITBYTE LN_TYPE,NT_LIBRARY INITLONG LN_NAME,LNAME INITBYTE LIB_FLAGS,LIBF_SUMUSED!LIBF_CHANGED INITWORD LIB_VERSION,VER INITWORD LIB_REVISION,REV INITLONG LIB_IDSTRING,IDSTR DC.W 0 ; Tämä on alustusfunktio, jota OpenLibrary() kutsuu sen jälkeen, kun ; kirjasto on pantu pystyyn. Tallennan segmentin osoitteen (BCPL-muodossa) ; sekä avaan tarvittavat kirjastot. OpenLib on yksinkertainen makro, ; joka kutsuu OpenLibrary():ä ja moveaa basepointterin data-alueelle ; kirjastonoden perään (sh-struktuuri pitää sisällään Library-noden ; sekä kirjaston omia muuttujia). ASL:sta ja Commoditiesista edellytetään ; versio 37 tai suurempi. initc movem.l a4/a6,-(sp) movea.l d0,a4 move.l a0,sh_SegList(a4) movea.l 4,a6 move.l a6,sh_SysBase(a4) OpenLib Dos,sh_DosBase(a4) ; DOS OpenLib intuition,sh_IntuitionBase(a4) ; Intuition OpenLib AName,sh_AslBase(a4),37 ; ASL OpenLib CName,sh_CxBase(a4),37 ; Commodities move.l a4,d0 movem.l (sp)+,a4/a6 rts ; Nämä ovat Open() ja Close(). Ne vain päivittävät opencountin. Open() ; palauttaa kirjaston kantaosoitteen, joka on sitä kutsuttaessa a6:ssa. ; Close() tarkistaa lisäksi DELEXP-bitin arvon. Se tarkoittaa, että ; "delayed expunge" on päällä, eli joku on kutsunut Expunge():a sillä ; aikaa, kuin kirjastoa on käytetty (katso alla). Jos DELEXP on päällä, ; suoritus lasketaan läpi Expunge()-funktioon. open addq.w #1,LIB_OPENCNT(a6) bclr.b #LIBB_DELEXP,LIB_FLAGS(a6) move.l a6,d0 rts close subq.w #1,LIB_OPENCNT(a6) btst.b #LIBB_DELEXP,LIB_FLAGS(a6) beq extfunc ; Expunge() poistaa kirjaston muistista. Ensin varmistetaan, että kirjasto ; ei ole kenelläkään auki. Jos näin on, ei sitä tietenkään poisteta, vaan ; asetetaan DELEXP-lippu viivästetyn expungen merkiksi. Kirjasto expungetaan ; heti, kun viimeinen sen käyttäjä sulkee sen. Muussa tapauksessa suljetaan ; kirjastot, vapautetaan data-alueen käyttämät muistialueet ja poistetaan ; segmentti muistista. Minun mielestäni tämä on kyllä hieman vaarallista, ; mutta näin se neuvotaan tekemään. expunge movem.l d2/a5/a6,-(sp) clr.l d2 movea.l a6,a5 movea.l 4,a6 tst.w LIB_OPENCNT(a5) beq expu0 bset.b #LIBB_DELEXP,LIB_FLAGS(a5) bra expu2 expu0 CloseLib sh_DosBase(a5) CloseLib sh_IntuitionBase(a5) move.l sh_CxBase(a5),d0 beq expu3 movea.l d0,a1 Lib CloseLibrary expu3 move.l sh_AslBase(a5),d0 beq expu1 movea.l d0,a1 Lib CloseLibrary expu1 move.l sh_SegList(a5),d2 movea.l a5,a1 Lib Remove clr.l d0 movea.l a5,a1 move.w LIB_NEGSIZE(a5),d0 suba.l d0,a1 add.w LIB_POSSIZE(a5),d0 Lib FreeMem expu2 move.l d2,d0 movem.l (sp)+,d2/a5/a6 rts extfunc clr.l d0 rts Kirjaston lataaminen Kirjaston voi ottaa käyttöön käsin lataamalla segmentin LoadSeg():llä (DOS-funk- tio), valmistelemalla sen MakeLibrary():llä ja lisäämällä sen Execin kirjasto- listaan AddLibrary():llä. Helpompaa on kuitenkin käyttää yllä kuvattua RomTagia (Resident-struktuuria), jolloin OS osaa ladata segmentin automaattisesti, kun se on sijoitettu LIBS:-hakemistoon, aina kun joku sitä haluaa käyttää (kutsuu Open- Library():ä). Kirjasto voi liittyä myös Zorro-korttiin tai muuhun laajennukseen, jolloin se voidaan sijoittaa SYS:Expansion-hakemistoon. Tällöin startup-sequencessa ajetta- va BindDrivers-komento lataa sen. Kortin toiminnasta huolehtiva kirjasto voidaan myös sijoittaa kortilla olevaan ROMiin. Mutta tämä kuuluu Expansion-kirjaston toimialaan, joten siitä ei enempää tässä. Nämä ovat Execin tarjoamat kirjastofunktiot: void AddLibrary( struct Library *library ); Lisää kirjaston Execin kirjastolistaan. Normaalisti Exec itse kutsuu tätä funk- tiota, kun joku haluaa avata levyllä olevan sen jälkeen, kun se on ensin kutsu- nut MakeLibrary():ä. Viimeksi mainittua en käsittele tässä ollenkaan, koska se kuuluu moduulinluontifunktioihin. Näihin palataan ehkä myöhemmin kurssin aikana. void RemLibrary( struct Library *library ); Poistaa kirjaston Execin kirjastolistasta. struct Library *OldOpenLibrary( UBYTE *libName ); Vanha avaamisfunktio. Ei tule käyttää. struct Library *OpenLibrary( UBYTE *libName, unsigned long version ); Avaa kirjaston. Parametreinä annetaan kirjaston nimi ja versionumero. Versionu- meron tulee olla sellainen, että kirjastossa varmasti on kaikki funktiot, joita tullaan kutsumaan. Mikäli kyseessä on käyttöjärjestelmän kirjasto, tulee versio- numeron olla LIBRARY_VERSION, joka on tätä nykyä 33. Se on vanhin virallisesti tuettu versionumero. void CloseLibrary( struct Library *library ); Sulkee kirjaston. Parametrinä annetaan kirjaston basepointer. APTR SetFunction( struct Library *library, long funcOffset, unsigned long (*newFunction)() ); Vaihtaa kirjaston funktion osoittimen. Tällä funktiolla voidaan kirjaston alku- peräinen funktiokoodi korvata omalla koodilla. Vektori vaihdetaan niin, että tarkistussumma on aina oikein (katso alla). Tällä funktiolla ei voi vaihtaa epästandardien kirjastojen (esimerkiksi dos.library) vektoreita. Funktiolle annetaan osoitin uuteen funktiokoodiin, ja sen jälkeen, kun kyseistä funktiota kutsutaan, suoritetaan uusi koodi. Mikäli uusi koodi ei jää muistiin, vaan lähtee pois esimerkiksi ohjelman suorituksen loputtua, tulee alkuperäinen vektori palauttaa. SetFuntion() palauttaa vanhan koodin osoitteen, joten se on helppo toteuttaa. void SumLibrary( struct Library *library ); Laskee kirjaston noden tarkistussumman. Funktio päivittää tarkistussumman kir- jaston nodestruktuuriin. Funktio myös tarkistaa vanhan summan, ja mikäli se ei täsmää, näyttää alertin. Aiheettomien alertien välttämiseksi kirjasto on mer- kittävä muutetuksi aina, kun sitä muutetaan. Tätä varten on lippu CHANGED. Oh- jelmat eivät yleensä kutsu tätä funktiota. {3Laiteohjaimet ja Execin I/O-toiminnot {3------------------------------------- Amigassa I/O-toiminnoista vastaavat Execin laiteohjaimet eli devicet. Laiteoh- jaimet ovat suoraan tekemisissä hardwaren kanssa. Ohjelmat käyttävät hardwarea laiteohjaimien kautta. Exec tarjoaa helpon liittymän laiteohjaimien hyödyntämi- seen. I/O-toiminnot tehdään lähettämällä laiteohjaimelle komentoja ja mahdolli- sesti dataa sekä otetaan niitä vastaan siltä. Käyttöjärjestelmään kuuluu 14 laiteohjainta: audio.device äänet clipboard.device leikkuulauta console.device konsoli (näppis ja näytin) gameport.device ilotikku, paddlet ja hiiri input.device syöte (monista lähteistä) keyboard.device näppis (matalalla tasolla) narrator.device puhe, selostus parallel.device rinnakkaisportti printer.device kirjoitin scsi.device Commodoren SCSI-väylä serial.device sarjaportti timer.device ajastin trackdisk.device sisäänrakennettu MFM-levyasemaväylä Laiteohjaimiin tutustutaan tarkemmin joskus muulloin. Tässä on tarkoitus vain nopeasti katsoa, miten niitä käytetään. Laiteohjain on avattava ennen käyttöä aivan kuin kirjastokin. Laiteohjaimen käyttöä varten tarvitsemme viestiportin ja IORequestin: struct IORequest { struct Message io_Message; struct Device *io_Device; /* Device-struktuuri */ struct Unit *io_Unit; /* Unit-struktuuri (privaatti) */ UWORD io_Command; /* I/O-komento */ UBYTE io_Flags; /* Liput (IOF_QUICK) */ BYTE io_Error; /* Virhenumero */ }; Tällä struktuurilla voidaan lähettää vain komentoja, joihin ei liity datan lähettämistä tai vastaanottamista. Jos dataakin liikkuu, tarvitaan pidempi ver- sio: struct IOStdReq { struct Message io_Message; struct Device *io_Device; /* Device-struktuuri */ struct Unit *io_Unit; /* Unit-struktuuri (privaatti) */ UWORD io_Command; /* I/O-komento */ UBYTE io_Flags; /* Liput (IOF_QUICK) */ BYTE io_Error; /* Virhenumero */ ULONG io_Actual; /* Siirtyneiden tavujen määrä */ ULONG io_Length; /* Tavujen määrä */ APTR io_Data; /* Osoittaa dataan */ ULONG io_Offset; /* Offset laitteella */ }; Laiteohjainta avatessa tarvitsee huolehtia vain siitä, että viestiosuus struk- tuurista on alustettu kunnolla. Tietysti kenttien tulee olla nollattu. Laiteoh- jain alustaa Device- ja Unit-kentät, ja muut alustetaan myöhemmin komentoja lähetettäessä. Käyttis V36 tarjoaa käteviä apufunktioita viestiportin ja IORe- questin tekemiseen. Laiteohjain aukeaa näin: struct MsgPort *port; struct IOStdReq *request; if(!(request = CreateIORequest(port = CreateMsgPort(), sizeof(struct IOStdReq)))) { printf("IORequestin tekeminen epäonnistui\n"); DeleteMsgPort(port); } if(OpenDevice("trackdisk.device",0,request,0)) printf("Ei auennut\n"); else { ....... } Huomattavaa tässä on erityisesti se, että OpenDevice() palauttaa nollan silloin, kun laiteohjaimen avaaminen onnistui. Yleensähän nolla tarkoittaa virhettä, mut- ta tässä se on toisin päin. Virheen tapauksessa palautetaan laiteohjainkohtainen virhenumero. Toinen parametri on yksikön numero. Tässä tapauksessa se tarkoittaa levyasemaa, nolla on (ensimmäinen) sisäinen levari. Viimeisenä voidaan antaa lippuja. Lopuksi laiteohjain pitää sulkea: CloseDevice(request); DeleteIORequest(request); DeleteMsgPort(port); Jokainen laiteohjain tunnistaa kahdeksan vakiokomentoa: CMD_RESET Resetoi laitteen alkutilaansa CMD_READ Lukee dataa laitteelta CMD_WRITE Kirjoittaa dataa laitteelle CMD_UPDATE Päivittää tietoja CMD_CLEAR Tyhjentää laiteohjaimen puskurit CMD_STOP Pysäyttää laitteen CMD_START Käynnistää laitteen CMD_FLUSH Puhdistaa komentojonon Kaikki laiteohjaimet eivät välttämättä tue kaikkia näitä, mikä on minusta varsin kummallista. Tuntemattoman laiteohjaimen käyttäminen voi olla vaarallista. Ko- mennon lähettäminen laiteohjaimelle on helppoa: request->io_Command = CMD_CLEAR; DoIO(request); Nämä peruskomennot toteuttavat yksinkertaisia toimintoja. Esimerkiksi FLUSH abortoi kaikki laiteohjaimella jonossa olevat I/O-toimintopyynnöt - myös muiden ohjelmien, joten sen kanssa kannattaa olla varovainen. CLEAR tyhjentää sarjapor- tin puskurit. UPDATE päivittää levylle trackdisk.devicen muistissa olevan uran, jos sitä on muutettu. STOP pysäyttää laitteen toiminnan. Laiteohjain lopettaa kyseisen yksikön jonossa olevien pyyntöjen käsittelyn ja jatkaa toimintaansa vasta, kun saa START-komennon. Moniyksikköisessä laitteessa komennot vaikuttavat vain siihen yksikköön (esimerkiksi nimenomaiseen levyasemaan), jolle komennot annetaan. Kun halutaan esimerkiksi kirjoittaa dataa, täytyy requestiin liittää tiedot kir- joitettavasta datasta, missä se on ja paljonko sitä on. Levykkeen tapauksessa pitää myös ilmoittaa, minne se kirjoitetaan. Kun kyseessä on massamuistiasema, muuttuja Offset kertoo datan sijaintipaikan levyllä: APTR data; request->io_Data = data; /* osoitin dataan */ request->io_Length = 0x200; /* kirjoitetaan vain yksi sektori */ request->io_Offset = 0x20000; /* paikka levyllä tavuina */ request->io_Command = CMD_WRITE; DoIO(request); Tämä koodi kirjoittaa 512 tavua dataa levyllä kohtaan $20000. Sektorin numero, jonka sisältö korvautuu uudella datalla, saadaan jakamalla Offset 512:lla. Off- setin ja pituuden tulee olla jaollisia sektorin koolla, joka tässä oletuksena on 512 tavua. Otin tässä trackdisk.devicen vain yksinkertaiseksi esimerkiksi - mas- samuistin käyttäminen laiteohjaintasolla ei ole suotavaa. DoIO() on synkroninen funktio. Se jää odottamaan vastausta ja ottaa viestin sit- ten portista. SendIO() vain lähettää viestin, joten vastausta on itse odotettava ja otettava viesti vastausportista. Sitä odotellessa voi myös kysellä, onko pyyntö jo käsitelty. Komennon voi myös abortoida. Tässä on esimerkki: SendIO(request); while(TRUE) { ...... if(CheckIO(request)) { GetMsg(port); break; } Voisihan tuota GetMsg():tä tietysti kutsua suoraankin, mutta näin se näyttää ki- vemmalta... Jos on tarve keskeyttää operaatio: if(!(CheckIO(request))) { AbortIO(request); WaitIO(request); } Tämä koodinpätkä tutkii, vieläkö komento on kesken. Jos se on, abortoidaan se ja odotetaan, että se todellakin on abortoitu. Laiteohjaimen tekeminen Tähän asiaan en paneudu nyt tässä. Laiteohjaimet tullaan käsittelemään omana osanaan tällä kurssilla. Siinä yhteydessä teemme myös oman laiteohjaimen. Voin tässä kuitenkin johdantona valottaa prosessia hieman. Laiteohjain on toiminnal- taan melko lailla samanlainen kuin kirjasto. Myös sillä on kantaosoite, io_Devi- ce osoittaa Device-struktuuriin, joka on aivan samanlainen kuin Library-struk- tuuri. Myös laiteohjaimilla on hyppytaulukko, ja jotkin laiteohjaimet tarjoavat kirjas- tojentyylisen funktioliittymän. Pääasiassa toimet kuitenkin hoidetaan IOReques- tien avulla. Laiteohjaimen on yleensä tarpeen laukaista lapsitehtävä erikseen jokaista yksikköä varten. Laiteohjaimen tekeminen on hankalampaa kuin kirjaston. Perehdymme siihen hamassa tulevaisuudessa. Nämä ovat Execin I/O-funktiot: void AddDevice( struct Device *device ); Lisää laiteohjaimen Execin listaan. void RemDevice( struct Device *device ); Poistaa laiteohjaimen. BYTE OpenDevice( UBYTE *devName, unsigned long unit, struct IORequest *ioRequest, unsigned long flags ); Avaa laiteohjaimen. Parametreinä annetaan sen nimi ja yksikkö, osoitin asianmu- kaisesti alustettuun requestblokkiin ja liput. void CloseDevice( struct IORequest *ioRequest ); Sulkee laiteohjaimen. BYTE DoIO( struct IORequest *ioRequest ); Lähettää komennon laiteohjaimelle synkronisesti. void SendIO( struct IORequest *ioRequest ); Lähettää komennon laiteohjaimelle asynkronisesti. BOOL CheckIO( struct IORequest *ioRequest ); Tarkistaa komennon suorituksen. Palauttaa nollan, jos komento ei ole vielä val- mis. BYTE WaitIO( struct IORequest *ioRequest ); Odottaa komennon valmistumista. Voidaan kutsua esimerkiksi AbortIO():n jälkeen tai suoraan SendIO():n jälkeen toiminnan synkronoimiseksi. Huomaa, että WaitIO() myös ottaa viestin vastausportista. Sitä voidaankin käyttää myös jo valmiiksi tiedetyn I/O-requestin valmisteluun seuraavaa tehtävää varten. WaitIO() odottaa viestiä porttiin kutsumalla Wait():iä. Mikäli komento on jo valmis, ei Wait():iä kutsuta, vaan Wait() ottaa viestin portista ja palaa välittömästi. Yleensä paras vaihtoehto I/O-toiminnan päättymisen odottamiseen on kutsua itse Wait():iä. Silloin voidaan odottaa myös esimerkiksi breakia ja ajastinta toteut- taen I/O:lle timeout-featuren. Jos aikaa kuluu liikaa, voi olla, että I/O:ssa on jotain vikaa, joten se abortoitaisiin. Syynä voisi olla vaikkapa, että kahden tietokoneen keskinäisessä tiedonsiirrossa toinen kone ei lähetä mitään, vaikka sen pitäisi. Mikäli käytetään WaitIO():ta odottamiseen (tai DoIO:ta), koko oh- jelma jumittuu, koska pyyntö ei koskaan täyty eikä funktio palaa. void AbortIO( struct IORequest *ioRequest ); Abortoi komennon. Ei odota abortin tapahtumista. {3Muisti {3------ Muistinvaraukseen on olemassa monimutkaisia funktioita, joita on myös muissa kirjastoissa kuin Execissä. Käsittelen tässä kuitenkin vain Execin muistinhal- lintafunktiot. Yksinkertaisimmillaan muistia varataan näin: APTR mem; if(!(mem=AllocMem(BUFFER_SIZE,MEMF_ANY))) printf("Ei saatu muistia\n"); Muisti vapautetaan lopuksi näin: if(mem) FreeMem(mem,BUFFER_SIZE); Halutun muistin tyypin määräävät AllocMem():lle annettavat liput: MEMF_ANY Mikä vain käy MEMF_PUBLIC Julkinen muisti MEMF_CHIP CHIP RAM MEMF_FAST FAST RAM MEMF_LOCAL Paikallista muistia, ei laajennuskortilla MEMF_24BITDMA 24-bittisen DMA:n ulottuvissa oleva muisti MEMF_CLEAR Tyhjennä alue ennen palaamista MEMF_LARGEST Suurin alue (katso alla) MEMF_REVERSE Etsi sopiva alue muistin yläpäästä MEMF_TOTAL Koko muisti (katso alla) Jos ei ole mitään tarvetta pyytää mitään erityistä muistia, tulee käyttää MEMF_ANY:ä. Tällöin varataan ensin FAST-muistia, jos sitä on vapaana, ellei, saat CHIPiä. PUBLIC tarkoittaa muistia, joka on toisten tehtävien saatavilla. Kaikkiin systeemistruktuureihin (esimerkiksi julkisiin viestiportteihin) käytet- ty muisti tulee varata PUBLIC:lla. PUBLIC-lipulla ei ole vielä merkitystä, mutta tulevaisuuden laajennuksiin tulee varautua käyttämällä sitä. Käyttöjärjestelmän ulkopuoliset virtuaalimuistiohjelmat tosin käyttävät PUBLIC-lippua - ne eivät hukkaa PUBLIC-muistia levylle, mutta se ei ole tämän lipun suunniteltu käyttötarkoitus. LOCAL on muistia, joka on suoraan prosessorin väylällä, yleensä emolevyllä. Laa- jennuskorteilla oleva muisti menetetään resetin yhteydessä, eikä sitä voida käyttää resetintakaisiin viritelmiin. 24BITDMA-lippu pyytää muistia, joka on Zorro II -muistiavaruudessa. Tämä lippu on tarkoitettu ainoastaan Zorro II -korttien ohjaimien käytettäväksi. Ohjelmien ei tulisi käyttää sitä koskaan. CLEAR-lipulla muistialue voidaan pyytää nollattavaksi ennen palaamista Alloc- Mem():stä. Normaalisti halutun suuruista aluetta etsitään muistin alapäästä, mutta REVERSE muuttaa toiminnan niin, että aluetta aletaankin hakea muistilistan lopusta. LARGEST ja TOTAL ovat lippuja, joita ei koskaan käytetä muistia varatessa. Ne voidaan antaa AvailMem():lle, joka kertoo vapaan muistin määrän: printf("Vapaata CHIP-muistia on %ld tavua\n",AvailMem(MEMF_CHIP)); Tätä funktiota ei yleensä tarvitse käyttää, ellei halua kertoa käyttäjälle tie- toa muistista. Muistin riittävyys selviää kyllä, kun sitä yritetään varata. AvailMem():n palauttama arvo ei välttämättä ole oikein. On hyvin todennäköistä, että muistia on jo varattu ja vapautettu monta kertaa, ennen kuin palautusarvoa päästään tutkimaan. Sen tarkkuuteen ei pidä luottaa liikaa. AvailMem() osaa kertoa vapaan muistin kokonaismäärän, jonka se palauttaa, kun sille annetaan lippu MEMF_TOTAL. Suurimman yhtenäisen vapaan tietyntyyppisen muistialueen koon saa näin: printf("Suurin yhtenäinen CHIP-muistin palanen on kooltaan %ld tavua\n", AvailMem(MEMF_CHIP|MEMF_LARGEST)); Exec tarjoaa versiosta 37 alkaen lisäksi funktiot AllocVec() ja FreeVec(). Ne toimivat aivan kuin AllocMem() ja FreeMem(), mutta lisäksi AllocVec() säilöö alueen pituuden, joten sitä ei tarvitse antaa ollenkaan aluetta vapautettaessa. AllocVec() varaa aina neljä tavua enemmän kuin pyydetään ja tallentaa alueen en- simmäiseen longwordiin sen pituuden. Muistin varaaminen ja vapauttaminen AllocVec():iä ja FreeVec():iä käyttäen käy näin: APTR mem; if(!(mem=AllocVec(BUFFER_SIZE,MEMF_ANY))) printf("Ei saatu muistia\n"); if(mem) FreeVec(mem); Näillä funktioilla pärjää hyvin. Käsittelen vielä lisää muistifunktioita, mutta voit hypätä suoraan luvun loppuun, jossa on kooste aihepiirin funktioista. Muistin siirtäminen paikasta toiseen Joskus tulee tarve siirtää dataa paikasta toiseen. Siihen on olemassa funktioi- ta: void CopyMem(source,dest,size); void CopyMemQuick(source,dest,size); Funktioiden toiminta on identtinen, mutta CopyMemQuick() on optimoitu versio, ja sitä käytettäessä kaikkien parametrien on oltava jaollisia neljällä (data ko- pioidaan longwordeja siirtelemällä). CopyMem() ja CopyMemQuick() ovat tehokkaita funktioita datan siirtämiseen, vaikka ne eivät tuekaan päällekkäisten alueiden siirtoa. Toisin sanoen kohdealue ja lähdealue eivät saa olla osittain samassa paikassa. Osittain päällekkäin sijaitsevien alueiden kopioimiseen käyviä funk- tiota on esimerkiksi omassa kirjastossani. Useat yhtäaikaiset muistinvaraukset Jos tarvitset useita vieläpä erityyppisiä muistialueita, voit varata ja vapaut- taa ne helposti kerralla. Siihen tarkoitukseen ovat olemassa funktiot AllocEnt- ry() ja FreeEntry(). Näitä käyttämällä muisti myös pysyy hyvin järjestyksessä omassa listassaan. Jokaisella tehtävällä on matalalla tasolla tällainen lista, mutta on kuitenkin syytä tehdä oma, koska se on lähinnä systeemin sisäiseen käyttöön. Tosin sitä voidaan hyödyntää automaattisen muistinvapautuksen aikaan- saamiseksi. Tehtävät-luvussa on lisää tietoa tästä. Muistilista ei ole standardi Execin lista, vaan noden sisältävä yksinkertainen struktuuri, johon kuuluu yksi tai useampi MemEntry: struct MemList { struct Node ml_Node; UWORD ml_NumEntries; /* Listassa olevien entryjen lukumäärä */ struct MemEntry ml_ME[1]; /* ensimmäinen entry */ }; struct MemEntry { union { ULONG meu_Reqs; /* AllocMem()-liput */ APTR meu_Addr; /* Tämän alueen osoite */ } me_Un; ULONG me_Length; /* Tämän alueen pituus */ }; Varsinkin MemEntry vaatinee selvittämistä. Sen pituus siis on kahdeksan tavua. Ensimmäisenä on longword, joka sisältää AllocMem():lle annettavat liput - tämän alueen vaatimukset, mikäli kyseessä on vasta muistinvaraus, joka tullaan anta- maan AllocEntry():lle. Seuraavana on toinen longword, joka sisältää alueen pi- tuuden. Osoitin MemListiin annetaan AllocEntry():lle. Mitä seuraavaksi tapahtuu, onkin hieman erikoista. Tälle alkuperäiselle muisti- listalle ei tehdä mitään, vaan AllocEntry() varaa uuden, johon se kopioi tiedot tästä listasta. Kuitenkaan tässä uudessa listassa MemEntryn ensimmäinen muuttuja ei sisällä lippuja, vaan se korvataan osoittimella varattuun muistialueeseen! AllocEntry() palauttaa osoittimen tähän uuteen listaan, ja se annetaan vapautet- taessa FreeEntry():lle. Virhetilanteessa kaikki jo mahdollisesti varatut muistialueet vapautetaan, ja AllocEntry() palauttaa epäonnistuneen varauksen liput bitti 31 asetettuna, joka testaamaalla saadaan tietää onnistuiko varaus. Varaus, sen onnistumisen tarkis- tus ja alueiden vapauttaminen onnistuu näin: #define ALLOCERROR 0x80000000 struct MemList *ml; /* valmiiksi alustettu MemList + MemEntryt */ struct MemList *memlist; /* tässä vaiheessa vielä NULL */ memlist = AllocEntry(ml); if(memlist & ALLOCERROR) printf("Ei saatu muistia\n"); else { ...... FreeEntry(memlist); } Tehtävää vaikeuttaa MemEntry-struktuurissa käytetty unioni. Sen saamiseksi määriteltyä oikein tai yleensä ollenkaan kannattaa lukea lehden C-kurssi! Lisää muistifunktioita Muistia voi varata myös absoluuttisesta osoitteesta, mutta se ei missään nimessä ole suotavaa. Älä tee niin kuin ehdottomasta pakosta. AllocAbs() toimii kuten AllocMem(), ensimmäisenä parametrinä annetaan alueen pituus, mutta toisena alueen osoite: APTR mem; if(!(mem=AllocAbs(0x10000,0x40000))) printf("Ei saatu muistia\n"); Muisti vapautetaan aivan normaalisti FreeMem()-funktiolla. AllocAbs():lle voi olla käyttöä joillakin erityissovelluksilla, esimerkiksi järjestelmän toimintaan liittyvillä ohjelmilla tms. Normaalien ohjelmien ei tulisi koskaan käyttää tätä funktiota. Edelleen löytyy vielä yksi pari funktioita, Allocate() ja Deallocate(). Ne käyttävät MemHeaderia ja MemChunkia: struct MemHeader { struct Node mh_Node; UWORD mh_Attributes; /* ei käytetä */ struct MemChunk *mh_First; /* ensimmäinen vapaa alue */ APTR mh_Lower; /* muistin alaraja */ APTR mh_Upper; /* muistin yläraja + 1 */ ULONG mh_Free; /* vapaiden tavujen määrä */ }; struct MemChunk { struct MemChunk *mc_Next; /* osoitin seuraavaan chunkiin */ ULONG mc_Bytes; /* tavujen määrä chunkissa */ }; Käyttöjärjestelmä pitää kirjaa vapaasta muistista käyttäen näitä struktuureja. Execin muistilistan lisäksi muistia voidaan manageroida myös paikallisesti. Ideana on alustaa ensin MemHeader- ja MemChunk-struktuurit kuvaamaan käytössäsi olevaa muistialuetta. Tämän jälkeen tästä paikallisesti ylläpidetystä muistilam- mikosta voidaan varata muistia Allocate():lla ja vapauttaa sitä Dealloca- te():lla. Tarvetta tälle ei juuri ole - itse en ole hyödyntänyt sitä koskaan. Yksi mahdol- linen sovellus on se, että ohjelmasi laukaisee muita tehtäviä, ja haluat seurata niiden muistinkäyttöä ja ylläpitää omaa muistijärjestelmää. Mielenkiintoinen yk- sityiskohta systeemin muistinhallinnassa on, että kirjaa pidetään vain vapaista muistialueista - kun muistialue varataan, Execillä ei ole hajuakaan, kenellä se on! Execin muistifunktiot ovat tässä: APTR Allocate( struct MemHeader *freeList, unsigned long byteSize ); Varaa muistia yksityisesti ylläpidetystä muistilammikosta. Parametreinä funk- tiolle annetaan osoitin MemHeaderiin ja varattavan alueen koko. void Deallocate( struct MemHeader *freeList, APTR memoryBlock, unsigned long byteSize ); Vapauttaa yksityisesti ylläpidetystä muistilammikosta varatun muistialueen. Pa- rametrit ovat osoitin MemHeaderiin, muistialueen osoite ja koko. APTR AllocMem( unsigned long byteSize, unsigned long requirements ); Varaa muistia, parametreinä annetaan alueen koko ja vaatimukset. APTR AllocAbs( unsigned long byteSize, APTR location ); Varaa muistia absoluuttisesta osoitteesta. Parametreinä annetaan alueen koko ja osoite, josta muistia halutaan. void FreeMem( APTR memoryBlock, unsigned long byteSize ); Vapauttaa AllocMem():llä tai AllocAbs():lla varatun muistialueen. Funktiolle an- netaan osoitin alueeseen sekä sen koko. ULONG AvailMem( unsigned long requirements ); Kertoo, kuinka paljon määritellyntyyppistä muistia on vapaana. Voi myös kertoa kokonaismuistin määrän sekä suurimman yhtenäisen alueen koon. struct MemList *AllocEntry( struct MemList *entry ); Varaa yhden tai useita muistialueita käyttäen muistilistaa. Palauttaa osoittimen uuteen muistilistaan, jossa on osoittimet varattuihin alueisiin, tai yhdenkin varauksen epäonnistuessa, epäonnistuneen varauksen vaatimukset bitti 31 asetet- tuna. void FreeEntry( struct MemList *entry ); Vapauttaa yhden tai useamman muistialueen, jotka ovat muistilistassa. Listan tu- lee olla AllocEntry():n palauttama - ei alkuperäinen lista, jossa on osoittimien sijaan vaatimukset. {3Tehtävät {3-------- Amiga ajaa ohjelmakoodia prosesseina ja tehtävinä. Myös prosessit ovat tehtäviä, mutta ne ovat laajempia kokonaisuuksia. Käsittelen tässä nyt tehtäviä Execin näkökulmasta. Tehtävät pidetään tehtävälistoissa tällaisten datastruktuurien avulla: struct Task { struct Node tc_Node; UBYTE tc_Flags; /* Liput */ UBYTE tc_State; /* Tehtävän tila */ BYTE tc_IDNestCnt; /* Keskeytysestot */ BYTE tc_TDNestCnt; /* Tehtävänvaihtoestot */ ULONG tc_SigAlloc; /* Varatut signaalit */ ULONG tc_SigWait; /* Signaalit, joita odotetaan */ ULONG tc_SigRecvd; /* Saadut signaalit */ ULONG tc_SigExcept; /* Poikkeuttavat signaalit */ UWORD tc_TrapAlloc; /* Varatut ansat */ UWORD tc_TrapAble; /* Sallitut ansat */ APTR tc_ExceptData; /* Poikkeustiladataosoitin */ APTR tc_ExceptCode; /* Poikkeustilakoodi */ APTR tc_TrapData; /* Ansadataosoitin */ APTR tc_TrapCode; /* Ansakoodi */ APTR tc_SPReg; /* Pinorekisteri */ APTR tc_SPLower; /* Pinon alaraja */ APTR tc_SPUpper; /* Pinon yläraja + 1 */ VOID (*tc_Switch)(); /* Kun CPU lähtee... */ VOID (*tc_Launch)(); /* Kun CPU tulee... */ struct List tc_MemEntry; /* Varattu muisti */ APTR tc_UserData; /* Käyttäjän dataosoitin */ }; Tehtävään liittyy paljon tietoa. Näistä tärkeimmät ovat tuolla lopussa. Tehtävää vaihdettaessa on syytä tietää, missä sen pino on ja missä kohtaa siinä mennään (SPReg ladataan SP-rekisteriin). Switch ja Launch osoittavat koodiin niihin koh- tiin, joihin hypitään, kun tehtäviä vaihdetaan. Ne tietysti vaihtuvat koko ajan, kun koodia ajetaan. Seuraavalla kerralla jatketaan siitä, mihin viimeksi jäätiin. Tämän enempää ei välttämättä ohjelmoijan tarvitse tehtävien teknisestä toteutuk- sesta tietää. Käsittelen tässä luvussa vielä lisää tehtäviin liittyviä toiminto- ja, mutta en usko, että niitä koskaan tarvitset. Kiinnostuksesta voit toki lukea loppuunkin... Tehtävänhallintafunktiot Execissä ovat nämä: APTR AddTask( struct Task *task, APTR initPC, APTR finalPC ); Lisää taskin Execin ajovalmiiden tehtävien listaan. Tehtävää ruvetaan ajamaan joko heti, jos sen prioriteetti on suurempi kuin tehtävä, jota jo ajetaan, tai sitten, kun sen aika tulee. Parameterinä annetaan osoitin alustettuun Task-st- ruktuuriin, tehtävän koodin osoite ja tehtävän cleanup-koodin osoite. void RemTask( struct Task *task ); Poistaa tehtävän. Keskeyttää tehtävän ajamisen, vapauttaa sen varaaman muistin ja poistaa kaiken siihen liittyvän tiedon muistista. struct Task *FindTask( UBYTE *name ); Etsii tehtävän tehtävälistoista. Tehtävä voi ottaa selville oman Task-struktuu- rinsa osoitteen kutsumalla FindTask(NULL):ia. BYTE SetTaskPri( struct Task *task, long priority ); Asettaa tehtävän prioriteetin. ULONG SetExcept( unsigned long newSignals, unsigned long signalSet ); Asettaa signaalit, joiden halutaan aiheuttavan poikkeustila. {3Task Creation {3------------- Tehtäviä voi tehdä itsekin. Monet ohjelmat laukaisevat lapsitehtävän tai jopa useita. Myös laiteohjaimet ajavat jokaiselle yksikölle oman tehtävän huolehti- maan erikseen sille tulevista komennoista. Tehtävän voi tehdä joko käsin tai käyttämällä apufunktioita. Erittäin tehokas tehtävänkäynnistysfunktio löytyy omasta sh.librarystäni. Operaatio ei ole kovinkaan monimutkikas. Meidän tarvit- see alustaa Task-struktuuri ja varata tehtävälle muistia pinoa varten. Tehtävää käynnistettäessä Exec täyttää alustamattomat kentät oletusarvoilla esi- merkiksi tuoden sisään oletuskoodin poikkeustiloista selviämiseen ja ansoihin joutumiseen ym. Alustettavia kenttiä ovat vain pinomuuttujat ja node sekä muis- tilista, jota kannattaa tässä ehdottomasti hyödyntää. Kaikki tehtävään liittyvä muisti (Task-struktuuri itse, pinoalue tms.) kannattaa varata AllocEntry():llä ja liittää muistilista tc_MemEntryyn, jolloin Exec vapauttaa automaattisesti kaikki muistialueet, kun tehtävän ajaminen päättyy! Laitan tähän nyt lyhyen konekielisen ohjelman, joka tekee meille tehtävän. En- siksi se varaa muistialueet valmiina olevan MemListin mukaan ja alustaa tehtävän listan, jossa se pitää MemEntryt, ja lisää AllocEntry():n palauttaman listan siihen. Pinoa tehtävälle varataan huimat 256 tavua, joka riitää, kun se ei tee mitään. Oikealle ohjelmalle pinoa tarvitaan 4000 tavua. Ohjelma myös kopioi tehtävän nimen Task-struktuurin perään sekä koodin pinoa- lueen perään! Näin tämä isäntäohjelma voi exitoida ja jättää lapsensa pyörimään. Jos lapsen koodia ajettaisiin isännän sisällä, ei sitä voitaisi lopettaa ennen lapsen tappamista. Tietysti tällä tavalla kopioitava koodi on oltava PC-relatii- vista. Kirjastossani oleva tehtävänkäynnistysfunktio toimii samaan tapaan, ja se osaa myös varata data-alueen tehtävälle. Lopuksi tehtävä käynnistetään kutsumalla AddTask()-funktiota. Sille annetaan osoitin Task-struktuuriin sekä initialPC ja finalPC. Näistä ensimmäinen osoittaa tehtävän koodin alkuun, ensimmäiseen käskyyn, jonka uusi tehtävä suorittaa. Jälkimmäinen osoite työnnetään pinoon eli siihen hypätään, jos tehtävä joskus suorittaa RTS-komennon. Jos se on nolla, Exec tuo siihen oletuskoodin osoitteen. Oletuskoodi vain kutsuu RemTask()-funktiota, joka poistaa tehtävän, mutta fina- lisaatiokoodi voi suorittaa muitakin cleanup-toimenpiteitä. Ohjelma on kauan sitten opetustarkoituksessa kirjoittamani esimerkki. ; ; Task v1.02! Written by Sami Klemola. ; Finished on Sunday the 3rd of March at 19:40. ; Commented on Sat 25-May-91 at 12:40. ; Final adjustments made on Mon 17-Jun-91 at 13:00. ; ; Copyright 1991 by Sami Klemola. All Rights Reserved. ; include "Macros" include "exec/memory.i" include "exec/tasks.i" movea.l 4,a6 cmpi.b #'0',(a0) beq.s RTask cmpi.b #'1',(a0) bne.s exit lea MList(pc),a0 Lib AllocEntry ; varataan muisti bmi.s exit ; pois, jos ei saatu movea.l d0,a4 movea.l ML_ME(a4),a5 move.l ML_ME+ME_LENGTH(a4),d2 lea TC_MEMENTRY(a5),a0 ; alustetaan tc_MemEntry NEWLIST a0 movea.l a4,a1 ; ja lisätään MemList siihen Lib Enqueue move.l d2,TC_SPLOWER(a5) ; pinon alaraja (varatun muistin addi.l #$100,d2 ; alkuosoite, pino kasvaa alaspäin) move.l d2,TC_SPUPPER(a5) ; pinon yläraja (alkuosoite + 256) move.l d2,TC_SPREG(a5) ; pino-osoitin (sama kuin yläraja) lea Start(pc),a0 ; tehtävän koodi movea.l d2,a1 movea.l d2,a2 ; koodin osoite a2:een AddTaskille moveq #MList-Start-1,d2 tcopy move.b (a0)+,(a1)+ ; kopioidaan koodi pinoalueen perään dbf d2,tcopy move.b #NT_TASK,LN_TYPE(a5) ; nodetyyppi task move.b #$80,LN_PRI(a5) ; prioriteetti -128 lea TName(pc),a0 lea TC_SIZE(a5),a1 move.l a1,LN_NAME(a5) ncopy move.b (a0)+,(a1)+ ; kopioidaan tehtävän nimi bne.s ncopy ; struktuurin perään movea.l a5,a1 ; task-struktuurin osoite suba.l a3,a3 ; oletuslopetuskoodi Lib AddTask ; lisäätän tehtävä tehtävälistaan exit rts RTask lea TName(pc),a1 ; etsitään tehtävä Lib FindTask movea.l d0,a1 beq.s error Lib RemTask ; ja lopetetaan sen ajaminen error rts Start clr.l d0 ; tehtävän ohjelmakoodi Cont move.w d0,$dff180 ; kirjoitetaan d0 COLOR00:aan addi.w #$1,d0 ; lisätään d0:aan 1 bra.s Cont ; ja uudestaan MList dc.l 0,0 ; MemList dc.b NT_MEMORY,0 dc.l TName dc.w 2 ; MemEntryjen määrä dc.l MEMF_PUBLIC!MEMF_CLEAR ; Task-struktuuri + tila nimelle dc.l TC_SIZE+10 dc.l MEMF_CLEAR ; Pino + koodi dc.l $100+MList-Start TName dc.b 'ExtraTask',0 ; tehtävän nimi {3Task Exclusion {3-------------- Edistynyt järjestelmäohjelma voi joskus havaita tarvitsevansa pääsyä johonkin globaaliin datastruktuuriin. Moniajosta johtuen datat voivat kuitenkin muuttua kesken kaiken. Tarvitaan jonkinlainen keino sen estämiseksi. Tehtävä voi hetkel- lisesti estää moniajon, mutta se tulee palauttaa välittömästi, kun datat on luettu. Moniajo estetään kutsumalla funkiota Forbid(). Muita tehtäviä ei ajeta, enen kuin kutsut funktiota Permit(), joka jälleen sallii moniajon. Mikäli kutsut Wait()-funktiota suoraan tai epäsuorasti Forbid():n jälkeen, odottaminen katkai- see eston, ja muita tehtäviä ajetaan odottaessasi. Kun odotettu signaali saa- daan, Wait() palaa ja moniajo estetään uudelleen. Keskeytykset ajetaan normaa- listi myös moniajon ollessa kiellettynä. Toinen mahdollisuus on disablointi kutsumalla funktiota Disable(). Disable() kieltää keskeytykset, joten moniajokaan ei toimi. Keskeytykset sallitaan jälleen funktiolla Enable(). Keskeytyksiä ei pitäisi kieltää yli 250 mikrosekunnin ajak- si kerrallaan, koska Amigan käyttöjärjestelmä on hyvin riippuvainen ajallaan ta- pahtuvista keskeytyksistä. Erityisesti serial.device tykkää kyttyrää, ellei se pääse lukemaan merkkejä ajoissa - ne menetetään ikuisesti. Forbid() ja Disable() ovat kasaantuvia funktiota. Jos kutsut niitä useamman ker- ran, joudut kutsumaan myös Permit():iä tai Enable():a yhtä monta kertaa. Kut- suista pidetään kirjaa Task-struktuurin NestCount-muuttujissa. Koskaan ei ole tarvetta kutsua sekä Forbid():iä että Disable():a. Disable() estää keskeytykset, joten se kieltää myös moniajon, koska Execin tehtävänvaihto tapahtuu keskeytyk- sessä. Joskus on tarpeen käyttää näitä funktiota, mutta ne tilanteet ovat harvassa. Normaaleilla ohjelmilla niitä ei pitäisi tulla ollenkaan. Useimmat kirjastot tarjoavat erityisiä funktioita toisten tehtävien sulkemiseksi pois tietystä järjestelmästä. Näitä ovat #?Lock#?()-funktiot, jotka hetkellisesti lukitsevat halutun datan, jotta se voidaan rauhassa lukea, estämättä moniajoa tai keskey- tyksiä ja näin ollen systeemiystävällisemmin. On olemassa vielä yksi tapa, jota voidaan käyttää, kun kyseessä ei ole systeemi- data, vaan jonkin ohjelman omat datat. Jos jotkin muutkin tehtävät haluavat päästä käsiksi dataan, voidaan käyttää opastimia. Semaphoret ovat käteviä ohjel- mien kesken tapahtuvaan tiedon ristiosoitukseen. En kuitenkaan käsittele niitä tässä, koska niille ei yleensä ole tarvetta. {3Task Exceptions {3--------------- Poikkeustilat ovat ohjelmallisia keskeytyksiä, jotka aiheutuvat tiettyjen sig- naalien aktivoituessa. Execin "exception":lla ei ole mitään tekemistä Motorolan "exception":n kanssa. Tällä tasolla jälkimmäiset tunnetaan "Task Trap":eina. Normaalisti ohjelma odottaa signaaleja Wait()-funktiolla, mutta SetEx- cept()-funktiolla voidaan asettaa tietyt signaalit aikaansaamaan poikkeustila. Tällä tavalla toimiessa signaaleja ei tarvitse odottaa, vaan suoritus siirtyy automaattisesti poikkeustilakoodiin, kun haluttu signaali aktivoituu. Tätä tar- koitusta varten tulee toimittaa erityinen poikkeustilakoodi (tc_ExceptCode). Tähän koodiin hypätään poikkeustilan sattuessa. Koodille annetaan D0:ssa signaa- limaski, jossa ovat päällä saatuja signaaleja vastaavat bitit. Nämä tulee myös palauttaa D0:ssa. A6:ssa on SysBase ja A1 osoittaa poikkeustiladataan (tc_Ex- ceptData). Varsinainen keskeytys poikkeustila ei ole. ExceptCode ajetaan normaalissa tilas- sa ja tehtävän pinoa käyttäen. Ennen koodiin hyppäämistä Exec työntää kaikki re- kisterit pinoon. Koodin tulee tarkistaa, mitkä signaalit aiheuttivat poikkeusti- lan, ja toimia sen mukaan käsitellen kaikki saadut signaalit. Poikkeustilakoodin ajaminen päättyy RTS-käskyyn. Tämän jälkeen Exec palauttaa alkuperäisen tilan- teen, ja tehtävän suoritus jatkuu normaalisti. Poikkeustilojen käyttäminen on vaarallista! ExceptCodeen hypätään välittömästi, kun poikkeustilan aiheuttavaksi määrätty signaali aktivoituu, mikä tarkoittaa sitä, että näin voi käydä kesken kriittisen koodin. Seurauksena voi olla tehtävän virhetoiminta. Mikäli tehtävä suorittaa juuri esimerkiksi jotain sys- teemifunktiota, se voi olla lukinnut jonkin resurssin. Jos kutsut samaa tai vas- taavaa funktiota myös ExceptCodesta, voi tuloksena olla deadlock. ExceptCodesi odottaa saavansa resurssin, joka on keskeytetyllä tehtävälläsi, joka ei sitä si- ten voi palauttaa, eikä se sitä koskaan saa. {3Task Traps {3---------- Ansat ovat prosessorin "exception":eja. Ansat toimivat samalla tavalla kuin poikkeustilatkin. Kun trappi aiheutuu, hypätään koodiin, johon osoittaa tc_Trap- Code. Execin oletustrappikoodi näyttää alertin kertoen, mikä oli trapin aiheut- taja. Mikäli trappinumeron bitti 31 on asetettu, on kyseessä korjaamaton virhe, joka johtaa väistämättä reboottiin. Tehtävä voi toimittaa oman koodin hoitamaan trapit, jolloin vakavistakin vir- heistä voidaan selvitä. Trapit voivat olla myös ohjelmiston virheitä tai tarkoi- tuksella koodista TRAP-komennolla aikaansaatuja hyppyjä. Trappikoodi ajetaan Su- pervisor-moodissa SysStackissa. Pinoon työnnetään "exception":n numero sekä pro- sessorikohtainen "exception frame". TrapCodesta palataan RTE-käskyllä. Huomaa poistaa trappinumero pinosta ennen pa- laamista. Ovelalla manipuloinnilla voidaan trappihandler tehdä niin, että se siirtää kontrollin oletushandlerille, jos trappi ei ole se, jota se odottaa. Mi- nun mielestäni olisi kyllä järkevintä tehdä handler, joka osaa selvitä kaikista trapeista. Trapeista selviäminen ei välttämättä aina ole helppoa, mutta useimmiten se on mahdollista. En kuitenkaan tässä käsittele aihetta enempää, koska se ei varsi- naisesti kuulu tämän alkeiskurssin piiriin. Hardwaretason "exception":t on mah- dollista hoidella myös matalammalla tasolla koukkimalla prosessorin vektorit. Se ei sitten enää kuuluu minkäänlaisen järjestelmäohjelmointikurssin aiheisiin. {3Tulevaisuutta kohti {3------------------- Kurssin tämä osa loppuu valitettavasti jo tähän juuri, kun kaikki alkoivat päästä vauhtiin. Aivan kaikkea ei ehditty käsitellä. Tuleviin osiin jäivät vielä esimerkiksi semaphoret ja keskeytykset, jotka eivät välttämättä aivan alkeis- kurssin asiaa olekaan. Jotkin asiat tässä osassa on käsitelty aika ylimalkaises- ti. Kurssiohjelmaan kuuluu jatkossa täydentäviä osia. Kovin vähälle jäi lähdekoodin osuus. Esimerkkejä tuli paljon, mutta kunnollista ohjelmakoodia ei yhtään. Tilaa sille ei oikeastaan jäänyt. Kannattaa haeskella BBS:istä lähdekoodeja ja ohjelmointiohjeita. Ainakin The Spitissä on paljon oh- jelmien lähdekoodeja. Ohjelmoinnista on myös olemassa ihan opetusmielessä jul- kaistuja ohjelmanpätkiä, joissa on ohjeita mukana.